Repository: neoclide/coc.nvim Branch: master Commit: 59ceb2d02e43 Files: 508 Total size: 5.1 MB Directory structure: gitextract_xmsz3i2m/ ├── .all-contributorsrc ├── .editorconfig ├── .github/ │ ├── .codecov.yml │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── ci.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .ignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .swcrc ├── .vim/ │ └── coc-settings.json ├── Backers.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── autoload/ │ ├── coc/ │ │ ├── api.vim │ │ ├── client.vim │ │ ├── color.vim │ │ ├── compat.vim │ │ ├── cursor.vim │ │ ├── dialog.vim │ │ ├── dict.vim │ │ ├── float.vim │ │ ├── highlight.vim │ │ ├── hlgroup.vim │ │ ├── inline.vim │ │ ├── list.vim │ │ ├── math.vim │ │ ├── notify.vim │ │ ├── prompt.vim │ │ ├── pum.vim │ │ ├── rpc.vim │ │ ├── snippet.vim │ │ ├── string.vim │ │ ├── task.vim │ │ ├── terminal.vim │ │ ├── text.vim │ │ ├── ui.vim │ │ ├── util.vim │ │ ├── vim9.vim │ │ ├── vtext.vim │ │ └── window.vim │ ├── coc.vim │ └── health/ │ └── coc.vim ├── bin/ │ ├── fuzzy.wasm │ ├── prompt.js │ ├── strwidth.wasm │ └── terminateProcess.sh ├── data/ │ └── schema.json ├── doc/ │ ├── coc-api.txt │ ├── coc-config.txt │ ├── coc-example-config.lua │ ├── coc-example-config.vim │ └── coc.txt ├── esbuild.js ├── eslint.config.mjs ├── history.md ├── jest.js ├── lua/ │ └── coc/ │ ├── diagnostic.lua │ ├── highlight.lua │ ├── text.lua │ ├── util.lua │ └── vtext.lua ├── package.json ├── plugin/ │ ├── coc.lua │ └── coc.vim ├── release.sh ├── src/ │ ├── __tests__/ │ │ ├── autoload/ │ │ │ ├── coc/ │ │ │ │ └── source/ │ │ │ │ ├── email.vim │ │ │ │ └── vim9.vim │ │ │ ├── legacy.vim │ │ │ └── vim9.vim │ │ ├── client/ │ │ │ ├── configuration.test.ts │ │ │ ├── connection.test.ts │ │ │ ├── converter.test.ts │ │ │ ├── diagnostics.test.ts │ │ │ ├── dynamic.test.ts │ │ │ ├── features.test.ts │ │ │ ├── fileSystemWatcher.test.ts │ │ │ ├── integration.test.ts │ │ │ ├── progressPart.test.ts │ │ │ ├── server/ │ │ │ │ ├── configServer.js │ │ │ │ ├── crashOnShutdownServer.js │ │ │ │ ├── crashServer.js │ │ │ │ ├── customServer.js │ │ │ │ ├── diagnosticServer.js │ │ │ │ ├── dynamicServer.js │ │ │ │ ├── errorServer.js │ │ │ │ ├── eventServer.js │ │ │ │ ├── fileWatchServer.js │ │ │ │ ├── nullServer.js │ │ │ │ ├── testDocuments.js │ │ │ │ ├── testFileWatcher.js │ │ │ │ ├── testInitializeResult.js │ │ │ │ ├── testServer.js │ │ │ │ └── timeoutOnShutdownServer.js │ │ │ ├── textSynchronization.test.ts │ │ │ ├── utils.test.ts │ │ │ └── workspaceFolder.test.ts │ │ ├── coc-settings.json │ │ ├── completion/ │ │ │ ├── basic.test.ts │ │ │ ├── float.test.ts │ │ │ ├── language.test.ts │ │ │ ├── sources.test.ts │ │ │ └── util.test.ts │ │ ├── configuration/ │ │ │ ├── configurationModel.test.ts │ │ │ ├── configurations.test.ts │ │ │ ├── settings.json │ │ │ └── util.test.ts │ │ ├── core/ │ │ │ ├── autocmds.test.ts │ │ │ ├── documents.test.ts │ │ │ ├── editors.test.ts │ │ │ ├── fileSystemWatcher.test.ts │ │ │ ├── files.test.ts │ │ │ ├── funcs.test.ts │ │ │ ├── keymaps.test.ts │ │ │ ├── locations.test.ts │ │ │ ├── terminals.test.ts │ │ │ ├── ui.test.ts │ │ │ └── workspaceFolder.test.ts │ │ ├── handler/ │ │ │ ├── callHierarchy.test.ts │ │ │ ├── codeActions.test.ts │ │ │ ├── codelens.test.ts │ │ │ ├── commands.test.ts │ │ │ ├── documentColors.test.ts │ │ │ ├── documentLinks.test.ts │ │ │ ├── fold.test.ts │ │ │ ├── format.test.ts │ │ │ ├── highlights.test.ts │ │ │ ├── hover.test.ts │ │ │ ├── index.test.ts │ │ │ ├── inlayHint.test.ts │ │ │ ├── inline.test.ts │ │ │ ├── inlineCompletion.test.ts │ │ │ ├── inlineValue.test.ts │ │ │ ├── linkedEditing.test.ts │ │ │ ├── locations.test.ts │ │ │ ├── outline.test.ts │ │ │ ├── parser.ts │ │ │ ├── refactor.test.ts │ │ │ ├── rename.test.ts │ │ │ ├── search.test.ts │ │ │ ├── selectionRange.test.ts │ │ │ ├── semanticTokens.test.ts │ │ │ ├── signature.test.ts │ │ │ ├── symbols.test.ts │ │ │ ├── typeHierarchy.test.ts │ │ │ └── workspace.test.ts │ │ ├── helper.ts │ │ ├── list/ │ │ │ ├── commandTask.test.ts │ │ │ ├── history.test.ts │ │ │ ├── manager.test.ts │ │ │ ├── mappings.test.ts │ │ │ ├── session.test.ts │ │ │ ├── source-funcs.test.ts │ │ │ ├── sources.test.ts │ │ │ ├── ui.test.ts │ │ │ └── worker.test.ts │ │ ├── markdown/ │ │ │ ├── index.test.ts │ │ │ └── renderer.test.ts │ │ ├── memos.json │ │ ├── modules/ │ │ │ ├── attach.test.ts │ │ │ ├── chars.test.ts │ │ │ ├── cursors.test.ts │ │ │ ├── db.test.ts │ │ │ ├── diagnosticBuffer.test.ts │ │ │ ├── diagnosticCollection.test.ts │ │ │ ├── diagnosticManager.test.ts │ │ │ ├── dialog.test.ts │ │ │ ├── document.test.ts │ │ │ ├── events.test.ts │ │ │ ├── extensionInstaller.test.ts │ │ │ ├── extensionManager.test.ts │ │ │ ├── extensionModules.test.ts │ │ │ ├── extensions.test.ts │ │ │ ├── fetch.test.ts │ │ │ ├── filter.test.ts │ │ │ ├── floatFactory.test.ts │ │ │ ├── fs.test.ts │ │ │ ├── fuzzyMatch.test.ts │ │ │ ├── highlighter.test.ts │ │ │ ├── line.test.ts │ │ │ ├── logger.test.ts │ │ │ ├── map.test.ts │ │ │ ├── memos.test.ts │ │ │ ├── menu.test.ts │ │ │ ├── outputChannel.test.ts │ │ │ ├── picker.test.ts │ │ │ ├── plugin.test.ts │ │ │ ├── quickpick.test.ts │ │ │ ├── regions.test.ts │ │ │ ├── sandbox/ │ │ │ │ └── log.js │ │ │ ├── semanticTokensBuilder.test.ts │ │ │ ├── server.js │ │ │ ├── services.test.ts │ │ │ ├── sources.test.ts │ │ │ ├── strWidth.test.ts │ │ │ ├── task.test.ts │ │ │ ├── terminal.test.ts │ │ │ ├── util.test.ts │ │ │ ├── window.test.ts │ │ │ └── workspace.test.ts │ │ ├── npm │ │ ├── rg │ │ ├── sample/ │ │ │ └── .vim/ │ │ │ └── coc-settings.json │ │ ├── snippets/ │ │ │ ├── manager.test.ts │ │ │ ├── parser.test.ts │ │ │ ├── session.test.ts │ │ │ └── snippet.test.ts │ │ ├── tree/ │ │ │ ├── basicProvider.test.ts │ │ │ └── treeView.test.ts │ │ ├── ultisnips.py │ │ ├── vim.test.ts │ │ └── vimrc │ ├── attach.ts │ ├── commands.ts │ ├── completion/ │ │ ├── complete.ts │ │ ├── floating.ts │ │ ├── index.ts │ │ ├── keywords.ts │ │ ├── match.ts │ │ ├── native/ │ │ │ ├── around.ts │ │ │ ├── buffer.ts │ │ │ └── file.ts │ │ ├── pum.ts │ │ ├── source-language.ts │ │ ├── source-vim.ts │ │ ├── source.ts │ │ ├── sources.ts │ │ ├── types.ts │ │ ├── util.ts │ │ └── wordDistance.ts │ ├── configuration/ │ │ ├── configuration.ts │ │ ├── event.ts │ │ ├── index.ts │ │ ├── model.ts │ │ ├── parser.ts │ │ ├── registry.ts │ │ ├── shape.ts │ │ ├── types.ts │ │ └── util.ts │ ├── core/ │ │ ├── autocmds.ts │ │ ├── channels.ts │ │ ├── contentProvider.ts │ │ ├── dialogs.ts │ │ ├── documents.ts │ │ ├── editors.ts │ │ ├── fileSystemWatcher.ts │ │ ├── files.ts │ │ ├── funcs.ts │ │ ├── highlights.ts │ │ ├── keymaps.ts │ │ ├── notifications.ts │ │ ├── terminals.ts │ │ ├── ui.ts │ │ ├── watchers.ts │ │ ├── watchman.ts │ │ └── workspaceFolder.ts │ ├── cursors/ │ │ ├── index.ts │ │ ├── session.ts │ │ ├── textRange.ts │ │ └── util.ts │ ├── diagnostic/ │ │ ├── buffer.ts │ │ ├── collection.ts │ │ ├── manager.ts │ │ └── util.ts │ ├── events.ts │ ├── extension/ │ │ ├── index.ts │ │ ├── installer.ts │ │ ├── manager.ts │ │ ├── stat.ts │ │ └── ui.ts │ ├── handler/ │ │ ├── callHierarchy.ts │ │ ├── codeActions.ts │ │ ├── codelens/ │ │ │ ├── buffer.ts │ │ │ └── index.ts │ │ ├── colors/ │ │ │ ├── colorBuffer.ts │ │ │ └── index.ts │ │ ├── commands.ts │ │ ├── fold.ts │ │ ├── format.ts │ │ ├── highlights.ts │ │ ├── hover.ts │ │ ├── index.ts │ │ ├── inlayHint/ │ │ │ ├── buffer.ts │ │ │ └── index.ts │ │ ├── inline.ts │ │ ├── linkedEditing.ts │ │ ├── links.ts │ │ ├── locations.ts │ │ ├── refactor/ │ │ │ ├── buffer.ts │ │ │ ├── changes.ts │ │ │ ├── index.ts │ │ │ └── search.ts │ │ ├── rename.ts │ │ ├── selectionRange.ts │ │ ├── semanticTokens/ │ │ │ ├── buffer.ts │ │ │ └── index.ts │ │ ├── signature.ts │ │ ├── symbols/ │ │ │ ├── buffer.ts │ │ │ ├── index.ts │ │ │ ├── outline.ts │ │ │ └── util.ts │ │ ├── typeHierarchy.ts │ │ ├── types.ts │ │ ├── util.ts │ │ └── workspace.ts │ ├── index.ts │ ├── language-client/ │ │ ├── LICENSE.txt │ │ ├── callHierarchy.ts │ │ ├── client.ts │ │ ├── codeAction.ts │ │ ├── codeLens.ts │ │ ├── colorProvider.ts │ │ ├── completion.ts │ │ ├── configuration.ts │ │ ├── declaration.ts │ │ ├── definition.ts │ │ ├── diagnostic.ts │ │ ├── documentHighlight.ts │ │ ├── documentLink.ts │ │ ├── documentSymbol.ts │ │ ├── executeCommand.ts │ │ ├── features.ts │ │ ├── fileOperations.ts │ │ ├── fileSystemWatcher.ts │ │ ├── foldingRange.ts │ │ ├── formatting.ts │ │ ├── hover.ts │ │ ├── implementation.ts │ │ ├── index.ts │ │ ├── inlayHint.ts │ │ ├── inlineCompletion.ts │ │ ├── inlineValue.ts │ │ ├── linkedEditingRange.ts │ │ ├── progress.ts │ │ ├── progressPart.ts │ │ ├── reference.ts │ │ ├── rename.ts │ │ ├── selectionRange.ts │ │ ├── semanticTokens.ts │ │ ├── signatureHelp.ts │ │ ├── textDocumentContent.ts │ │ ├── textSynchronization.ts │ │ ├── typeDefinition.ts │ │ ├── typeHierarchy.ts │ │ ├── utils/ │ │ │ ├── async.ts │ │ │ ├── codeConverter.ts │ │ │ ├── errorHandler.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ └── uuid.ts │ │ ├── workspaceFolders.ts │ │ └── workspaceSymbol.ts │ ├── languages.ts │ ├── list/ │ │ ├── basic.ts │ │ ├── commandTask.ts │ │ ├── configuration.ts │ │ ├── db.ts │ │ ├── formatting.ts │ │ ├── history.ts │ │ ├── manager.ts │ │ ├── mappings.ts │ │ ├── prompt.ts │ │ ├── session.ts │ │ ├── source/ │ │ │ ├── commands.ts │ │ │ ├── diagnostics.ts │ │ │ ├── extensions.ts │ │ │ ├── folders.ts │ │ │ ├── links.ts │ │ │ ├── lists.ts │ │ │ ├── location.ts │ │ │ ├── notifications.ts │ │ │ ├── outline.ts │ │ │ ├── services.ts │ │ │ ├── sources.ts │ │ │ └── symbols.ts │ │ ├── types.ts │ │ ├── ui.ts │ │ └── worker.ts │ ├── logger/ │ │ ├── index.ts │ │ └── log.ts │ ├── markdown/ │ │ ├── index.ts │ │ ├── renderer.ts │ │ └── styles.ts │ ├── model/ │ │ ├── bufferSync.ts │ │ ├── chars.ts │ │ ├── db.ts │ │ ├── dialog.ts │ │ ├── document.ts │ │ ├── download.ts │ │ ├── editInspect.ts │ │ ├── fetch.ts │ │ ├── floatFactory.ts │ │ ├── fuzzyMatch.ts │ │ ├── highlighter.ts │ │ ├── input.ts │ │ ├── line.ts │ │ ├── memos.ts │ │ ├── menu.ts │ │ ├── mru.ts │ │ ├── notification.ts │ │ ├── outputChannel.ts │ │ ├── picker.ts │ │ ├── popup.ts │ │ ├── progress.ts │ │ ├── quickpick.ts │ │ ├── regions.ts │ │ ├── relativePattern.ts │ │ ├── resolver.ts │ │ ├── semanticTokensBuilder.ts │ │ ├── status.ts │ │ ├── strwidth.ts │ │ ├── tabs.ts │ │ ├── task.ts │ │ ├── terminal.ts │ │ ├── textdocument.ts │ │ └── textline.ts │ ├── plugin.ts │ ├── provider/ │ │ ├── callHierarchyManager.ts │ │ ├── codeActionManager.ts │ │ ├── codeLensManager.ts │ │ ├── declarationManager.ts │ │ ├── definitionManager.ts │ │ ├── documentColorManager.ts │ │ ├── documentHighlightManager.ts │ │ ├── documentLinkManager.ts │ │ ├── documentSymbolManager.ts │ │ ├── foldingRangeManager.ts │ │ ├── formatManager.ts │ │ ├── formatRangeManager.ts │ │ ├── hoverManager.ts │ │ ├── implementationManager.ts │ │ ├── index.ts │ │ ├── inlayHintManager.ts │ │ ├── inlineCompletionItemManager.ts │ │ ├── inlineValueManager.ts │ │ ├── linkedEditingRangeManager.ts │ │ ├── manager.ts │ │ ├── onTypeFormatManager.ts │ │ ├── referenceManager.ts │ │ ├── renameManager.ts │ │ ├── selectionRangeManager.ts │ │ ├── semanticTokensManager.ts │ │ ├── semanticTokensRangeManager.ts │ │ ├── signatureManager.ts │ │ ├── typeDefinitionManager.ts │ │ ├── typeHierarchyManager.ts │ │ └── workspaceSymbolsManager.ts │ ├── services.ts │ ├── snippets/ │ │ ├── eval.ts │ │ ├── manager.ts │ │ ├── parser.ts │ │ ├── session.ts │ │ ├── snippet.ts │ │ ├── string.ts │ │ ├── util.ts │ │ └── variableResolve.ts │ ├── tree/ │ │ ├── BasicDataProvider.ts │ │ ├── LocationsDataProvider.ts │ │ ├── TreeItem.ts │ │ ├── TreeView.ts │ │ ├── filter.ts │ │ └── index.ts │ ├── types.ts │ ├── util/ │ │ ├── ansiparse.ts │ │ ├── array.ts │ │ ├── async.ts │ │ ├── charCode.ts │ │ ├── color.ts │ │ ├── constants.ts │ │ ├── convert.ts │ │ ├── diff.ts │ │ ├── errors.ts │ │ ├── extensionRegistry.ts │ │ ├── factory.ts │ │ ├── filter.ts │ │ ├── fs.ts │ │ ├── fuzzy.ts │ │ ├── index.ts │ │ ├── is.ts │ │ ├── jsonRegistry.ts │ │ ├── jsonSchema.ts │ │ ├── lodash.ts │ │ ├── map.ts │ │ ├── mutex.ts │ │ ├── node.ts │ │ ├── numbers.ts │ │ ├── object.ts │ │ ├── platform.ts │ │ ├── position.ts │ │ ├── processes.ts │ │ ├── protocol.ts │ │ ├── registry.ts │ │ ├── sequence.ts │ │ ├── string.ts │ │ ├── textedit.ts │ │ └── timing.ts │ ├── window.ts │ └── workspace.ts ├── tsconfig.json └── typings/ ├── LICENSE ├── Readme.md └── index.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "coc.nvim", "projectOwner": "neoclide", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 50, "commit": false, "commitConvention": "angular", "contributors": [ { "login": "chemzqm", "name": "Qiming zhao", "avatar_url": "https://avatars.githubusercontent.com/u/251450?v=4", "profile": "https://github.com/chemzqm", "contributions": [ "code" ] }, { "login": "fannheyward", "name": "Heyward Fann", "avatar_url": "https://avatars.githubusercontent.com/u/345274?v=4", "profile": "https://fann.im/", "contributions": [ "code" ] }, { "login": "weirongxu", "name": "Raidou", "avatar_url": "https://avatars.githubusercontent.com/u/1709861?v=4", "profile": "https://github.com/weirongxu", "contributions": [ "code" ] }, { "login": "kevinhwang91", "name": "kevinhwang91", "avatar_url": "https://avatars.githubusercontent.com/u/17562139?v=4", "profile": "https://github.com/kevinhwang91", "contributions": [ "code" ] }, { "login": "iamcco", "name": "年糕小豆汤", "avatar_url": "https://avatars.githubusercontent.com/u/5492542?v=4", "profile": "http://yuuko.cn/", "contributions": [ "code" ] }, { "login": "Avi-D-coder", "name": "Avi Dessauer", "avatar_url": "https://avatars.githubusercontent.com/u/29133776?v=4", "profile": "https://github.com/Avi-D-coder", "contributions": [ "code" ] }, { "login": "voldikss", "name": "最上川", "avatar_url": "https://avatars.githubusercontent.com/u/20282795?v=4", "profile": "https://github.com/voldikss", "contributions": [ "code" ] }, { "login": "yatli", "name": "Yatao Li", "avatar_url": "https://avatars.githubusercontent.com/u/20684720?v=4", "profile": "https://www.microsoft.com/en-us/research/people/yatli/", "contributions": [ "code" ] }, { "login": "xiyaowong", "name": "wongxy", "avatar_url": "https://avatars.githubusercontent.com/u/47070852?v=4", "profile": "https://github.com/xiyaowong", "contributions": [ "code" ] }, { "login": "sam-mccall", "name": "Sam McCall", "avatar_url": "https://avatars.githubusercontent.com/u/548993?v=4", "profile": "https://github.com/sam-mccall", "contributions": [ "code" ] }, { "login": "pappasam", "name": "Samuel Roeca", "avatar_url": "https://avatars.githubusercontent.com/u/3723671?v=4", "profile": "https://samroeca.com/pages/about.html#about", "contributions": [ "code" ] }, { "login": "amiralies", "name": "Amirali Esmaeili", "avatar_url": "https://avatars.githubusercontent.com/u/13261088?v=4", "profile": "https://github.com/amiralies", "contributions": [ "code" ] }, { "login": "jrowlingson", "name": "Jack Rowlingson", "avatar_url": "https://avatars.githubusercontent.com/u/3051781?v=4", "profile": "https://bit.ly/3cLKGE4", "contributions": [ "code" ] }, { "login": "tomtomjhj", "name": "Jaehwang Jung", "avatar_url": "https://avatars.githubusercontent.com/u/19489738?v=4", "profile": "https://github.com/tomtomjhj", "contributions": [ "code" ] }, { "login": "antoinemadec", "name": "Antoine", "avatar_url": "https://avatars.githubusercontent.com/u/10830594?v=4", "profile": "https://github.com/antoinemadec", "contributions": [ "code" ] }, { "login": "cosminadrianpopescu", "name": "Cosmin Popescu", "avatar_url": "https://avatars.githubusercontent.com/u/5187873?v=4", "profile": "https://github.com/cosminadrianpopescu", "contributions": [ "code" ] }, { "login": "xuanduc987", "name": "Duc Nghiem Xuan", "avatar_url": "https://avatars.githubusercontent.com/u/1186411?v=4", "profile": "https://ducnx.com/", "contributions": [ "code" ] }, { "login": "oblitum", "name": "Francisco Lopes", "avatar_url": "https://avatars.githubusercontent.com/u/1269815?v=4", "profile": "https://nosubstance.me/", "contributions": [ "code" ] }, { "login": "daquexian", "name": "daquexian", "avatar_url": "https://avatars.githubusercontent.com/u/11607199?v=4", "profile": "https://github.com/daquexian", "contributions": [ "code" ] }, { "login": "dependabot[bot]", "name": "dependabot[bot]", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "profile": "https://github.com/apps/dependabot", "contributions": [ "code" ] }, { "login": "greenkeeper[bot]", "name": "greenkeeper[bot]", "avatar_url": "https://avatars.githubusercontent.com/in/505?v=4", "profile": "https://github.com/apps/greenkeeper", "contributions": [ "code" ] }, { "login": "ckipp01", "name": "Chris Kipp", "avatar_url": "https://avatars.githubusercontent.com/u/13974112?v=4", "profile": "https://chris-kipp.io/", "contributions": [ "code" ] }, { "login": "dmitmel", "name": "Dmytro Meleshko", "avatar_url": "https://avatars.githubusercontent.com/u/15367354?v=4", "profile": "https://dmitmel.github.io/", "contributions": [ "code" ] }, { "login": "kirillbobyrev", "name": "Kirill Bobyrev", "avatar_url": "https://avatars.githubusercontent.com/u/3352968?v=4", "profile": "https://github.com/kirillbobyrev", "contributions": [ "code" ] }, { "login": "gbcreation", "name": "Gontran Baerts", "avatar_url": "https://avatars.githubusercontent.com/u/454315?v=4", "profile": "https://github.com/gbcreation", "contributions": [ "code" ] }, { "login": "andys8", "name": "Andy", "avatar_url": "https://avatars.githubusercontent.com/u/13085980?v=4", "profile": "https://andys8.de/", "contributions": [ "code" ] }, { "login": "GopherJ", "name": "Cheng JIANG", "avatar_url": "https://avatars.githubusercontent.com/u/33961674?v=4", "profile": "https://www.alexcj96.com/", "contributions": [ "code" ] }, { "login": "cpearce-py", "name": "Corin", "avatar_url": "https://avatars.githubusercontent.com/u/53532946?v=4", "profile": "https://github.com/cpearce-py", "contributions": [ "code" ] }, { "login": "wodesuck", "name": "Daniel Zhang", "avatar_url": "https://avatars.githubusercontent.com/u/3124581?v=4", "profile": "https://github.com/wodesuck", "contributions": [ "code" ] }, { "login": "Ferdi265", "name": "Ferdinand Bachmann", "avatar_url": "https://avatars.githubusercontent.com/u/4077106?v=4", "profile": "https://github.com/Ferdi265", "contributions": [ "code" ] }, { "login": "gou4shi1", "name": "Guangqing Chen", "avatar_url": "https://avatars.githubusercontent.com/u/16915589?v=4", "profile": "https://goushi.me/", "contributions": [ "code" ] }, { "login": "iamruinous", "name": "Jade Meskill", "avatar_url": "https://avatars.githubusercontent.com/u/2108?v=4", "profile": "http://jademeskill.com/", "contributions": [ "code" ] }, { "login": "jpoppe", "name": "Jasper Poppe", "avatar_url": "https://avatars.githubusercontent.com/u/65505?v=4", "profile": "https://github.com/jpoppe", "contributions": [ "code" ] }, { "login": "jean", "name": "Jean Jordaan", "avatar_url": "https://avatars.githubusercontent.com/u/84800?v=4", "profile": "https://github.com/jean", "contributions": [ "code" ] }, { "login": "kidonng", "name": "Kid", "avatar_url": "https://avatars.githubusercontent.com/u/44045911?v=4", "profile": "https://xuann.wang/", "contributions": [ "code" ] }, { "login": "Kavantix", "name": "Pieter van Loon", "avatar_url": "https://avatars.githubusercontent.com/u/6243755?v=4", "profile": "https://github.com/Kavantix", "contributions": [ "code" ] }, { "login": "rliebz", "name": "Robert Liebowitz", "avatar_url": "https://avatars.githubusercontent.com/u/5321575?v=4", "profile": "https://github.com/rliebz", "contributions": [ "code" ] }, { "login": "megalithic", "name": "Seth Messer", "avatar_url": "https://avatars.githubusercontent.com/u/3678?v=4", "profile": "https://megalithic.io/", "contributions": [ "code" ] }, { "login": "UncleBill", "name": "UncleBill", "avatar_url": "https://avatars.githubusercontent.com/u/1141198?v=4", "profile": "https://github.com/UncleBill", "contributions": [ "code" ] }, { "login": "ZSaberLv0", "name": "ZERO", "avatar_url": "https://avatars.githubusercontent.com/u/6846867?v=4", "profile": "http://zsaber.com/", "contributions": [ "code" ] }, { "login": "fsouza", "name": "fsouza", "avatar_url": "https://avatars.githubusercontent.com/u/108725?v=4", "profile": "https://fsouza.blog/", "contributions": [ "code" ] }, { "login": "onichandame", "name": "XiaoZhang", "avatar_url": "https://avatars.githubusercontent.com/u/23728505?v=4", "profile": "https://onichandame.com/", "contributions": [ "code" ] }, { "login": "whyreal", "name": "whyreal", "avatar_url": "https://avatars.githubusercontent.com/u/2084642?v=4", "profile": "https://github.com/whyreal", "contributions": [ "code" ] }, { "login": "yehuohan", "name": "yehuohan", "avatar_url": "https://avatars.githubusercontent.com/u/17680752?v=4", "profile": "https://github.com/yehuohan", "contributions": [ "code" ] }, { "login": "Bakudankun", "name": "バクダンくん", "avatar_url": "https://avatars.githubusercontent.com/u/4504807?v=4", "profile": "http://www.bakudan.farm/", "contributions": [ "code" ] }, { "login": "glepnir", "name": "Raphael", "avatar_url": "https://avatars.githubusercontent.com/u/41671631?v=4", "profile": "https://blog.gopherhub.org/", "contributions": [ "code" ] }, { "login": "tbodt", "name": "tbodt", "avatar_url": "https://avatars.githubusercontent.com/u/5678977?v=4", "profile": "https://tbodt.com/", "contributions": [ "code" ] }, { "login": "aaronmcdaid", "name": "Aaron McDaid", "avatar_url": "https://avatars.githubusercontent.com/u/64350?v=4", "profile": "https://aaronmcdaid.github.io/", "contributions": [ "code" ] }, { "login": "versi786", "name": "Aasif Versi", "avatar_url": "https://avatars.githubusercontent.com/u/7347942?v=4", "profile": "https://github.com/versi786", "contributions": [ "code" ] }, { "login": "abnerf", "name": "Abner Silva", "avatar_url": "https://avatars.githubusercontent.com/u/56300?v=4", "profile": "https://github.com/abnerf", "contributions": [ "code" ] }, { "login": "sheerun", "name": "Adam Stankiewicz", "avatar_url": "https://avatars.githubusercontent.com/u/292365?v=4", "profile": "http://sheerun.net/", "contributions": [ "code" ] }, { "login": "adamansky", "name": "Adamansky Anton", "avatar_url": "https://avatars.githubusercontent.com/u/496683?v=4", "profile": "https://wirow.io/", "contributions": [ "code" ] }, { "login": "ahmedelgabri", "name": "Ahmed El Gabri", "avatar_url": "https://avatars.githubusercontent.com/u/63876?v=4", "profile": "https://gabri.me/", "contributions": [ "code" ] }, { "login": "theg4sh", "name": "Alexandr Kondratev", "avatar_url": "https://avatars.githubusercontent.com/u/5094691?v=4", "profile": "http://theg4sh.ru/", "contributions": [ "code" ] }, { "login": "andrewkshim", "name": "Andrew Shim", "avatar_url": "https://avatars.githubusercontent.com/u/1403410?v=4", "profile": "https://github.com/andrewkshim", "contributions": [ "code" ] }, { "login": "alindeman", "name": "Andy Lindeman", "avatar_url": "https://avatars.githubusercontent.com/u/395621?v=4", "profile": "http://andylindeman.com/", "contributions": [ "code" ] }, { "login": "Augustin82", "name": "Augustin", "avatar_url": "https://avatars.githubusercontent.com/u/2370810?v=4", "profile": "https://github.com/Augustin82", "contributions": [ "code" ] }, { "login": "Eijebong", "name": "Bastien Orivel", "avatar_url": "https://avatars.githubusercontent.com/u/3650385?v=4", "profile": "https://bananium.fr/", "contributions": [ "code" ] }, { "login": "ayroblu", "name": "Ben Lu", "avatar_url": "https://avatars.githubusercontent.com/u/4915682?v=4", "profile": "https://github.com/ayroblu", "contributions": [ "code" ] }, { "login": "vantreeseba", "name": "Ben", "avatar_url": "https://avatars.githubusercontent.com/u/316782?v=4", "profile": "https://github.com/vantreeseba", "contributions": [ "code" ] }, { "login": "bmon", "name": "Brendan Roy", "avatar_url": "https://avatars.githubusercontent.com/u/2115272?v=4", "profile": "https://github.com/bmon", "contributions": [ "code" ] }, { "login": "brianembry", "name": "brianembry", "avatar_url": "https://avatars.githubusercontent.com/u/35347666?v=4", "profile": "https://github.com/brianembry", "contributions": [ "code" ] }, { "login": "b-", "name": "br", "avatar_url": "https://avatars.githubusercontent.com/u/284789?v=4", "profile": "https://keybase.io/bri_", "contributions": [ "code" ] }, { "login": "casonadams", "name": "Cason Adams", "avatar_url": "https://avatars.githubusercontent.com/u/17597548?v=4", "profile": "https://github.com/casonadams", "contributions": [ "code" ] }, { "login": "y9c", "name": "Chang Y", "avatar_url": "https://avatars.githubusercontent.com/u/5415510?v=4", "profile": "https://github.com/y9c", "contributions": [ "code" ] }, { "login": "yous", "name": "Chayoung You", "avatar_url": "https://avatars.githubusercontent.com/u/853977?v=4", "profile": "https://yous.be/", "contributions": [ "code" ] }, { "login": "chenlijun99", "name": "Chen Lijun", "avatar_url": "https://avatars.githubusercontent.com/u/20483759?v=4", "profile": "https://github.com/chenlijun99", "contributions": [ "code" ] }, { "login": "beeender", "name": "Chen Mulong", "avatar_url": "https://avatars.githubusercontent.com/u/449296?v=4", "profile": "https://github.com/beeender", "contributions": [ "code" ] }, { "login": "rsrchboy", "name": "Chris Weyl", "avatar_url": "https://avatars.githubusercontent.com/u/59620?v=4", "profile": "http://weyl.io/", "contributions": [ "code" ] }, { "login": "dezza", "name": "dezza", "avatar_url": "https://avatars.githubusercontent.com/u/402927?v=4", "profile": "https://github.com/dezza", "contributions": [ "code" ] }, { "login": "ceedubs", "name": "Cody Allen", "avatar_url": "https://avatars.githubusercontent.com/u/977929?v=4", "profile": "https://github.com/ceedubs", "contributions": [ "code" ] }, { "login": "pyrho", "name": "Damien Rajon", "avatar_url": "https://avatars.githubusercontent.com/u/145502?v=4", "profile": "https://www.25.wf/", "contributions": [ "code" ] }, { "login": "daern91", "name": "Daniel Eriksson", "avatar_url": "https://avatars.githubusercontent.com/u/6084427?v=4", "profile": "https://github.com/daern91", "contributions": [ "code" ] }, { "login": "danjenson", "name": "Daniel Jenson", "avatar_url": "https://avatars.githubusercontent.com/u/4793438?v=4", "profile": "https://github.com/danjenson", "contributions": [ "code" ] }, { "login": "davidmh", "name": "David Mejorado", "avatar_url": "https://avatars.githubusercontent.com/u/594302?v=4", "profile": "https://github.com/davidmh", "contributions": [ "code" ] }, { "login": "pderichai", "name": "Deric Pang", "avatar_url": "https://avatars.githubusercontent.com/u/13430946?v=4", "profile": "https://github.com/pderichai", "contributions": [ "code" ] }, { "login": "miyatsu", "name": "Ding Tao", "avatar_url": "https://avatars.githubusercontent.com/u/12852587?v=4", "profile": "https://www.dingtao.org/blog", "contributions": [ "code" ] }, { "login": "doronbehar", "name": "Doron Behar", "avatar_url": "https://avatars.githubusercontent.com/u/10998835?v=4", "profile": "https://github.com/doronbehar", "contributions": [ "code" ] }, { "login": "kovetskiy", "name": "Egor Kovetskiy", "avatar_url": "https://avatars.githubusercontent.com/u/8445924?v=4", "profile": "https://github.com/kovetskiy", "contributions": [ "code" ] }, { "login": "elkowar", "name": "ElKowar", "avatar_url": "https://avatars.githubusercontent.com/u/5300871?v=4", "profile": "https://github.com/elkowar", "contributions": [ "code" ] }, { "login": "demelev", "name": "Emeliov Dmitrii", "avatar_url": "https://avatars.githubusercontent.com/u/3952209?v=4", "profile": "https://github.com/demelev", "contributions": [ "code" ] }, { "login": "sawmurai", "name": "Fabian Becker", "avatar_url": "https://avatars.githubusercontent.com/u/6454986?v=4", "profile": "https://github.com/sawmurai", "contributions": [ "code" ] }, { "login": "FallenWarrior2k", "name": "FallenWarrior2k", "avatar_url": "https://avatars.githubusercontent.com/u/20320149?v=4", "profile": "https://github.com/FallenWarrior2k", "contributions": [ "code" ] }, { "login": "fnune", "name": "Fausto Núñez Alberro", "avatar_url": "https://avatars.githubusercontent.com/u/16181067?v=4", "profile": "https://fnune.com/", "contributions": [ "code" ] }, { "login": "FelipeCRamos", "name": "Felipe Ramos", "avatar_url": "https://avatars.githubusercontent.com/u/7572843?v=4", "profile": "https://github.com/FelipeCRamos", "contributions": [ "code" ] }, { "login": "frbor", "name": "Fredrik Borg", "avatar_url": "https://avatars.githubusercontent.com/u/2320183?v=4", "profile": "https://github.com/frbor", "contributions": [ "code" ] }, { "login": "gavsim", "name": "Gavin Sim", "avatar_url": "https://avatars.githubusercontent.com/u/812273?v=4", "profile": "http://www.gavinsim.co.uk/", "contributions": [ "code" ] }, { "login": "gibfahn", "name": "Gibson Fahnestock", "avatar_url": "https://avatars.githubusercontent.com/u/15943089?v=4", "profile": "https://fahn.co/", "contributions": [ "code" ] }, { "login": "giovannigiordano", "name": "Giovanni Giordano", "avatar_url": "https://avatars.githubusercontent.com/u/15145952?v=4", "profile": "https://github.com/giovannigiordano", "contributions": [ "code" ] }, { "login": "qubbit", "name": "Gopal Adhikari", "avatar_url": "https://avatars.githubusercontent.com/u/1987473?v=4", "profile": "https://github.com/qubbit", "contributions": [ "code" ] }, { "login": "hanh090", "name": "Hanh Le", "avatar_url": "https://avatars.githubusercontent.com/u/3643657?v=4", "profile": "https://github.com/hanh090", "contributions": [ "code" ] }, { "login": "hedyhli", "name": "hedy", "avatar_url": "https://avatars.githubusercontent.com/u/50042066?v=4", "profile": "https://github.com/hedyhli", "contributions": [ "code" ] }, { "login": "hendriklammers", "name": "Hendrik Lammers", "avatar_url": "https://avatars.githubusercontent.com/u/754556?v=4", "profile": "https://www.hendriklammers.com/", "contributions": [ "code" ] }, { "login": "henrybarreto", "name": "Henry Barreto", "avatar_url": "https://avatars.githubusercontent.com/u/23109089?v=4", "profile": "https://github.com/henrybarreto", "contributions": [ "code" ] }, { "login": "WhyNotHugo", "name": "Hugo", "avatar_url": "https://avatars.githubusercontent.com/u/730811?v=4", "profile": "https://hugo.barrera.io/", "contributions": [ "code" ] }, { "login": "jackieli-tes", "name": "Jackie Li", "avatar_url": "https://avatars.githubusercontent.com/u/64778297?v=4", "profile": "https://github.com/jackieli-tes", "contributions": [ "code" ] }, { "login": "MrQubo", "name": "Jakub Nowak", "avatar_url": "https://avatars.githubusercontent.com/u/16545322?v=4", "profile": "https://github.com/MrQubo", "contributions": [ "code" ] }, { "login": "euoia", "name": "James Pickard", "avatar_url": "https://avatars.githubusercontent.com/u/1271216?v=4", "profile": "https://github.com/euoia", "contributions": [ "code" ] }, { "login": "jsfaint", "name": "Jia Sui", "avatar_url": "https://avatars.githubusercontent.com/u/571829?v=4", "profile": "https://github.com/jsfaint", "contributions": [ "code" ] }, { "login": "expipiplus1", "name": "Ellie Hermaszewska", "avatar_url": "https://avatars.githubusercontent.com/u/857308?v=4", "profile": "https://github.com/expipiplus1", "contributions": [ "code" ] }, { "login": "cincodenada", "name": "Joel Bradshaw", "avatar_url": "https://avatars.githubusercontent.com/u/479715?v=4", "profile": "https://cincodenada.com/", "contributions": [ "code" ] }, { "login": "irizwaririz", "name": "John Carlo Roberto", "avatar_url": "https://avatars.githubusercontent.com/u/10111643?v=4", "profile": "https://github.com/irizwaririz", "contributions": [ "code" ] }, { "login": "Jomik", "name": "Jonas Holst Damtoft", "avatar_url": "https://avatars.githubusercontent.com/u/699655?v=4", "profile": "https://github.com/Jomik", "contributions": [ "code" ] }, { "login": "jdlehman", "name": "Jonathan Lehman", "avatar_url": "https://avatars.githubusercontent.com/u/3144695?v=4", "profile": "http://inlehmansterms.net/", "contributions": [ "code" ] }, { "login": "JoosepAlviste", "name": "Joosep Alviste", "avatar_url": "https://avatars.githubusercontent.com/u/9450943?v=4", "profile": "https://joosep.xyz/", "contributions": [ "code" ] }, { "login": "josa42", "name": "Josa Gesell", "avatar_url": "https://avatars.githubusercontent.com/u/423234?v=4", "profile": "https://github.com/josa42", "contributions": [ "code" ] }, { "login": "joshuarubin", "name": "Joshua Rubin", "avatar_url": "https://avatars.githubusercontent.com/u/194275?v=4", "profile": "https://jawa.dev/", "contributions": [ "code" ] }, { "login": "perrin4869", "name": "Julian Grinblat", "avatar_url": "https://avatars.githubusercontent.com/u/5774716?v=4", "profile": "https://github.com/perrin4869", "contributions": [ "code" ] }, { "login": "valentjn", "name": "Julian Valentin", "avatar_url": "https://avatars.githubusercontent.com/u/19839841?v=4", "profile": "https://valentjn.github.io/", "contributions": [ "code" ] }, { "login": "KabbAmine", "name": "KabbAmine", "avatar_url": "https://avatars.githubusercontent.com/u/5658084?v=4", "profile": "https://kabbamine.github.io/", "contributions": [ "code" ] }, { "login": "acro5piano", "name": "Kay Gosho", "avatar_url": "https://avatars.githubusercontent.com/u/10719495?v=4", "profile": "https://moncargo.io/", "contributions": [ "code" ] }, { "login": "hkennyv", "name": "Kenny Huynh", "avatar_url": "https://avatars.githubusercontent.com/u/29909203?v=4", "profile": "https://kennyvh.com/", "contributions": [ "code" ] }, { "login": "kevinrambaud", "name": "Kevin Rambaud", "avatar_url": "https://avatars.githubusercontent.com/u/7501477?v=4", "profile": "https://github.com/kevinrambaud", "contributions": [ "code" ] }, { "login": "kiancross", "name": "Kian Cross", "avatar_url": "https://avatars.githubusercontent.com/u/11011464?v=4", "profile": "https://github.com/kiancross", "contributions": [ "code" ] }, { "login": "kristijanhusak", "name": "Kristijan Husak", "avatar_url": "https://avatars.githubusercontent.com/u/1782860?v=4", "profile": "https://ko-fi.com/kristijanhusak", "contributions": [ "code" ] }, { "login": "NullVoxPopuli", "name": "NullVoxPopuli", "avatar_url": "https://avatars.githubusercontent.com/u/199018?v=4", "profile": "https://github.com/NullVoxPopuli", "contributions": [ "code" ] }, { "login": "lassepe", "name": "Lasse Peters", "avatar_url": "https://avatars.githubusercontent.com/u/10076790?v=4", "profile": "https://github.com/lassepe", "contributions": [ "code" ] }, { "login": "Linerre", "name": "Noel Errenil", "avatar_url": "https://avatars.githubusercontent.com/u/49512984?v=4", "profile": "https://github.com/Linerre", "contributions": [ "code" ] }, { "login": "LinArcX", "name": "LinArcX", "avatar_url": "https://avatars.githubusercontent.com/u/10884422?v=4", "profile": "https://github.com/LinArcX", "contributions": [ "code" ] }, { "login": "liuchengxu", "name": "Liu-Cheng Xu", "avatar_url": "https://avatars.githubusercontent.com/u/8850248?v=4", "profile": "https://paypal.me/liuchengxu", "contributions": [ "code" ] }, { "login": "foxtrot", "name": "Marc", "avatar_url": "https://avatars.githubusercontent.com/u/4153572?v=4", "profile": "https://malloc.me/", "contributions": [ "code" ] }, { "login": "mgaw", "name": "Marius Gawrisch", "avatar_url": "https://avatars.githubusercontent.com/u/2177016?v=4", "profile": "https://github.com/mgaw", "contributions": [ "code" ] }, { "login": "mhintz", "name": "Mark Hintz", "avatar_url": "https://avatars.githubusercontent.com/u/2789742?v=4", "profile": "http://www.markhz.com/", "contributions": [ "code" ] }, { "login": "MatElGran", "name": "Mathieu Le Tiec", "avatar_url": "https://avatars.githubusercontent.com/u/1052778?v=4", "profile": "https://github.com/MatElGran", "contributions": [ "code" ] }, { "login": "matt-fff", "name": "Matt White", "avatar_url": "https://avatars.githubusercontent.com/u/8656127?v=4", "profile": "https://matt-w.net/", "contributions": [ "code" ] }, { "login": "ml-evs", "name": "Matthew Evans", "avatar_url": "https://avatars.githubusercontent.com/u/7916000?v=4", "profile": "https://github.com/ml-evs", "contributions": [ "code" ] }, { "login": "Me1onRind", "name": "Me1onRind", "avatar_url": "https://avatars.githubusercontent.com/u/19531270?v=4", "profile": "https://github.com/Me1onRind", "contributions": [ "code" ] }, { "login": "Qyriad", "name": "Qyriad", "avatar_url": "https://avatars.githubusercontent.com/u/1542224?v=4", "profile": "https://github.com/Qyriad", "contributions": [ "code" ] }, { "login": "leonardssh", "name": "Narcis B.", "avatar_url": "https://avatars.githubusercontent.com/u/35312043?v=4", "profile": "https://leo.is-a.dev/", "contributions": [ "code" ] }, { "login": "Neur1n", "name": "Neur1n", "avatar_url": "https://avatars.githubusercontent.com/u/17579247?v=4", "profile": "https://github.com/Neur1n", "contributions": [ "code" ] }, { "login": "nicoder", "name": "Nicolas Dermine", "avatar_url": "https://avatars.githubusercontent.com/u/365210?v=4", "profile": "https://github.com/nicoder", "contributions": [ "code" ] }, { "login": "NoahTheDuke", "name": "Noah", "avatar_url": "https://avatars.githubusercontent.com/u/603677?v=4", "profile": "https://github.com/NoahTheDuke", "contributions": [ "code" ] }, { "login": "IndexXuan", "name": "PENG Rui", "avatar_url": "https://avatars.githubusercontent.com/u/6322673?v=4", "profile": "https://github.com/IndexXuan", "contributions": [ "code" ] }, { "login": "paco0x", "name": "Paco", "avatar_url": "https://avatars.githubusercontent.com/u/6123425?v=4", "profile": "https://liaoph.com/", "contributions": [ "code" ] }, { "login": "peng1999", "name": "Peng Guanwen", "avatar_url": "https://avatars.githubusercontent.com/u/12483662?v=4", "profile": "https://github.com/peng1999", "contributions": [ "code" ] }, { "login": "ilAYAli", "name": "Petter Wahlman", "avatar_url": "https://avatars.githubusercontent.com/u/1106732?v=4", "profile": "https://www.twitter.com/badeip", "contributions": [ "code" ] }, { "login": "pvonmoradi", "name": "Pooya Moradi", "avatar_url": "https://avatars.githubusercontent.com/u/1058151?v=4", "profile": "https://github.com/pvonmoradi", "contributions": [ "code" ] }, { "login": "QuadeMorrison", "name": "Quade Morrison", "avatar_url": "https://avatars.githubusercontent.com/u/10917383?v=4", "profile": "https://github.com/QuadeMorrison", "contributions": [ "code" ] }, { "login": "vogler", "name": "Ralf Vogler", "avatar_url": "https://avatars.githubusercontent.com/u/493741?v=4", "profile": "https://github.com/vogler", "contributions": [ "code" ] }, { "login": "crccw", "name": "Ran Chen", "avatar_url": "https://avatars.githubusercontent.com/u/41463?v=4", "profile": "https://github.com/crccw", "contributions": [ "code" ] }, { "login": "bigardone", "name": "Ricardo García Vega", "avatar_url": "https://avatars.githubusercontent.com/u/1090272?v=4", "profile": "https://bigardone.dev/", "contributions": [ "code" ] }, { "login": "nomasprime", "name": "Rick Jones", "avatar_url": "https://avatars.githubusercontent.com/u/140855?v=4", "profile": "https://github.com/nomasprime", "contributions": [ "code" ] }, { "login": "rschristian", "name": "Ryan Christian", "avatar_url": "https://avatars.githubusercontent.com/u/33403762?v=4", "profile": "https://github.com/rschristian", "contributions": [ "code" ] }, { "login": "winterbesos", "name": "Salo", "avatar_url": "https://avatars.githubusercontent.com/u/4694263?v=4", "profile": "http://salo.so/", "contributions": [ "code" ] }, { "login": "Hazelfire", "name": "Sam Nolan", "avatar_url": "https://avatars.githubusercontent.com/u/13807753?v=4", "profile": "https://github.com/Hazelfire", "contributions": [ "code" ] }, { "login": "rickysaurav", "name": "Saurav", "avatar_url": "https://avatars.githubusercontent.com/u/13986039?v=4", "profile": "https://github.com/rickysaurav", "contributions": [ "code" ] }, { "login": "smackesey", "name": "Sean Mackesey", "avatar_url": "https://avatars.githubusercontent.com/u/1531373?v=4", "profile": "https://github.com/smackesey", "contributions": [ "code" ] }, { "login": "sheeldotme", "name": "Sheel Patel", "avatar_url": "https://avatars.githubusercontent.com/u/6991406?v=4", "profile": "https://github.com/sheeldotme", "contributions": [ "code" ] }, { "login": "solomonwzs", "name": "Solomon Ng", "avatar_url": "https://avatars.githubusercontent.com/u/907942?v=4", "profile": "https://github.com/solomonwzs", "contributions": [ "code" ] }, { "login": "kadimisetty", "name": "Sri Kadimisetty", "avatar_url": "https://avatars.githubusercontent.com/u/535947?v=4", "profile": "https://github.com/kadimisetty", "contributions": [ "code" ] }, { "login": "stephenprater", "name": "Stephen Prater", "avatar_url": "https://avatars.githubusercontent.com/u/149870?v=4", "profile": "https://github.com/stephenprater", "contributions": [ "code" ] }, { "login": "kibs", "name": "Sune Kibsgaard", "avatar_url": "https://avatars.githubusercontent.com/u/14085?v=4", "profile": "https://kibs.dk/", "contributions": [ "code" ] }, { "login": "Aquaakuma", "name": "Aquaakuma", "avatar_url": "https://avatars.githubusercontent.com/u/31891793?v=4", "profile": "https://github.com/Aquaakuma", "contributions": [ "code" ] }, { "login": "coil398", "name": "Takumi Kawase", "avatar_url": "https://avatars.githubusercontent.com/u/7694377?v=4", "profile": "https://github.com/coil398", "contributions": [ "code" ] }, { "login": "theblobscp", "name": "The Blob SCP", "avatar_url": "https://avatars.githubusercontent.com/u/81673375?v=4", "profile": "https://github.com/theblobscp", "contributions": [ "code" ] }, { "login": "przepompownia", "name": "Tomasz N", "avatar_url": "https://avatars.githubusercontent.com/u/11404453?v=4", "profile": "https://github.com/przepompownia", "contributions": [ "code" ] }, { "login": "gasuketsu", "name": "Tomoyuki Harada", "avatar_url": "https://avatars.githubusercontent.com/u/15703757?v=4", "profile": "https://github.com/gasuketsu", "contributions": [ "code" ] }, { "login": "tonyfettes", "name": "Tony Fettes", "avatar_url": "https://avatars.githubusercontent.com/u/29998228?v=4", "profile": "https://github.com/tonyfettes", "contributions": [ "code" ] }, { "login": "tony", "name": "Tony Narlock", "avatar_url": "https://avatars.githubusercontent.com/u/26336?v=4", "profile": "https://www.git-pull.com/", "contributions": [ "code" ] }, { "login": "wwwjfy", "name": "Tony Wang", "avatar_url": "https://avatars.githubusercontent.com/u/126527?v=4", "profile": "https://blog.wwwjfy.net/", "contributions": [ "code" ] }, { "login": "Varal7", "name": "Victor Quach", "avatar_url": "https://avatars.githubusercontent.com/u/8019486?v=4", "profile": "https://github.com/Varal7", "contributions": [ "code" ] }, { "login": "whisperity", "name": "Whisperity", "avatar_url": "https://avatars.githubusercontent.com/u/1969470?v=4", "profile": "https://github.com/whisperity", "contributions": [ "code" ] }, { "login": "willtrnr", "name": "William Turner", "avatar_url": "https://avatars.githubusercontent.com/u/1878110?v=4", "profile": "https://github.com/willtrnr", "contributions": [ "code" ] }, { "login": "damnever", "name": "Xiaochao Dong", "avatar_url": "https://avatars.githubusercontent.com/u/6223594?v=4", "profile": "https://drafts.damnever.com/", "contributions": [ "code" ] }, { "login": "hyhugh", "name": "Hugh Hou", "avatar_url": "https://avatars.githubusercontent.com/u/16500351?v=4", "profile": "https://github.com/hyhugh", "contributions": [ "code" ] }, { "login": "jackielii", "name": "Jackie Li", "avatar_url": "https://avatars.githubusercontent.com/u/360983?v=4", "profile": "https://github.com/jackielii", "contributions": [ "code" ] }, { "login": "TheConfuZzledDude", "name": "Zachary Freed", "avatar_url": "https://avatars.githubusercontent.com/u/3160203?v=4", "profile": "https://github.com/TheConfuZzledDude", "contributions": [ "code" ] }, { "login": "akiyosi", "name": "akiyosi", "avatar_url": "https://avatars.githubusercontent.com/u/8478977?v=4", "profile": "https://github.com/akiyosi", "contributions": [ "code" ] }, { "login": "alexjg", "name": "alexjg", "avatar_url": "https://avatars.githubusercontent.com/u/224635?v=4", "profile": "https://github.com/alexjg", "contributions": [ "code" ] }, { "login": "aste4", "name": "aste4", "avatar_url": "https://avatars.githubusercontent.com/u/47511385?v=4", "profile": "https://github.com/aste4", "contributions": [ "code" ] }, { "login": "clyfish", "name": "clyfish", "avatar_url": "https://avatars.githubusercontent.com/u/541215?v=4", "profile": "https://github.com/clyfish", "contributions": [ "code" ] }, { "login": "dev7ba", "name": "dev7ba", "avatar_url": "https://avatars.githubusercontent.com/u/93706552?v=4", "profile": "https://github.com/dev7ba", "contributions": [ "code" ] }, { "login": "diartyz", "name": "diartyz", "avatar_url": "https://avatars.githubusercontent.com/u/4486152?v=4", "profile": "https://github.com/diartyz", "contributions": [ "code" ] }, { "login": "doza-daniel", "name": "doza-daniel", "avatar_url": "https://avatars.githubusercontent.com/u/13752683?v=4", "profile": "https://github.com/doza-daniel", "contributions": [ "code" ] }, { "login": "equal-l2", "name": "equal-l2", "avatar_url": "https://avatars.githubusercontent.com/u/8597717?v=4", "profile": "https://github.com/equal-l2", "contributions": [ "code" ] }, { "login": "FongHou", "name": "fong", "avatar_url": "https://avatars.githubusercontent.com/u/13973254?v=4", "profile": "https://github.com/FongHou", "contributions": [ "code" ] }, { "login": "hexh250786313", "name": "hexh", "avatar_url": "https://avatars.githubusercontent.com/u/26080416?v=4", "profile": "https://blog.hexuhua.vercel.app/", "contributions": [ "code" ] }, { "login": "hhiraba", "name": "hhiraba", "avatar_url": "https://avatars.githubusercontent.com/u/4624806?v=4", "profile": "https://github.com/hhiraba", "contributions": [ "code" ] }, { "login": "ic-768", "name": "ic-768", "avatar_url": "https://avatars.githubusercontent.com/u/83115125?v=4", "profile": "https://github.com/ic-768", "contributions": [ "code" ] }, { "login": "javiertury", "name": "javiertury", "avatar_url": "https://avatars.githubusercontent.com/u/1520320?v=4", "profile": "https://github.com/javiertury", "contributions": [ "code" ] }, { "login": "seiyeah78", "name": "karasu", "avatar_url": "https://avatars.githubusercontent.com/u/6185139?v=4", "profile": "https://github.com/seiyeah78", "contributions": [ "code" ] }, { "login": "kevineato", "name": "kevineato", "avatar_url": "https://avatars.githubusercontent.com/u/13666221?v=4", "profile": "https://github.com/kevineato", "contributions": [ "code" ] }, { "login": "m4c0", "name": "Eduardo Costa", "avatar_url": "https://avatars.githubusercontent.com/u/1664510?v=4", "profile": "https://github.com/m4c0", "contributions": [ "code" ] }, { "login": "micchy326", "name": "micchy326", "avatar_url": "https://avatars.githubusercontent.com/u/23257067?v=4", "profile": "https://github.com/micchy326", "contributions": [ "code" ] }, { "login": "midchildan", "name": "midchildan", "avatar_url": "https://avatars.githubusercontent.com/u/7343721?v=4", "profile": "https://keybase.io/midchildan", "contributions": [ "code" ] }, { "login": "minefuto", "name": "minefuto", "avatar_url": "https://avatars.githubusercontent.com/u/46558834?v=4", "profile": "https://github.com/minefuto", "contributions": [ "code" ] }, { "login": "miyanokomiya", "name": "miyanokomiya", "avatar_url": "https://avatars.githubusercontent.com/u/20733354?v=4", "profile": "https://twitter.com/robokomy", "contributions": [ "code" ] }, { "login": "miyaviee", "name": "miyaviee", "avatar_url": "https://avatars.githubusercontent.com/u/15247561?v=4", "profile": "https://github.com/miyaviee", "contributions": [ "code" ] }, { "login": "monkoose", "name": "monkoose", "avatar_url": "https://avatars.githubusercontent.com/u/6261276?v=4", "profile": "https://github.com/monkoose", "contributions": [ "code", "bug" ] }, { "login": "mujx", "name": "mujx", "avatar_url": "https://avatars.githubusercontent.com/u/6430350?v=4", "profile": "https://github.com/mujx", "contributions": [ "code" ] }, { "login": "mvilim", "name": "mvilim", "avatar_url": "https://avatars.githubusercontent.com/u/40682862?v=4", "profile": "https://github.com/mvilim", "contributions": [ "code" ] }, { "login": "naruaway", "name": "naruaway", "avatar_url": "https://avatars.githubusercontent.com/u/2931577?v=4", "profile": "https://naruaway.com/", "contributions": [ "code" ] }, { "login": "piersy", "name": "piersy", "avatar_url": "https://avatars.githubusercontent.com/u/5087847?v=4", "profile": "https://github.com/piersy", "contributions": [ "code" ] }, { "login": "ryantig", "name": "ryantig", "avatar_url": "https://avatars.githubusercontent.com/u/324810?v=4", "profile": "https://github.com/ryantig", "contributions": [ "code" ] }, { "login": "rydesun", "name": "rydesun", "avatar_url": "https://avatars.githubusercontent.com/u/19602440?v=4", "profile": "https://catcat.cc/", "contributions": [ "code" ] }, { "login": "sc00ter", "name": "sc00ter", "avatar_url": "https://avatars.githubusercontent.com/u/1271025?v=4", "profile": "https://github.com/sc00ter", "contributions": [ "code" ] }, { "login": "smhc", "name": "smhc", "avatar_url": "https://avatars.githubusercontent.com/u/6404304?v=4", "profile": "https://github.com/smhc", "contributions": [ "code" ] }, { "login": "stkaplan", "name": "Sam Kaplan", "avatar_url": "https://avatars.githubusercontent.com/u/594990?v=4", "profile": "https://github.com/stkaplan", "contributions": [ "code" ] }, { "login": "tasuten", "name": "tasuten", "avatar_url": "https://avatars.githubusercontent.com/u/1623176?v=4", "profile": "https://github.com/tasuten", "contributions": [ "code" ] }, { "login": "todesking", "name": "todesking", "avatar_url": "https://avatars.githubusercontent.com/u/112881?v=4", "profile": "http://todesking.com/", "contributions": [ "code" ] }, { "login": "typicode", "name": "typicode", "avatar_url": "https://avatars.githubusercontent.com/u/5502029?v=4", "profile": "https://github.com/typicode", "contributions": [ "code" ] }, { "login": "LiMingFei56", "name": "李鸣飞", "avatar_url": "https://avatars.githubusercontent.com/u/8553407?v=4", "profile": "https://limingfei56.github.io/", "contributions": [ "code" ] }, { "login": "eltociear", "name": "Ikko Ashimine", "avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4", "profile": "https://bandism.net/", "contributions": [ "doc" ] }, { "login": "rammiah", "name": "Rammiah", "avatar_url": "https://avatars.githubusercontent.com/u/26727562?v=4", "profile": "https://github.com/rammiah", "contributions": [ "bug" ] }, { "login": "lambdalisue", "name": "Alisue", "avatar_url": "https://avatars.githubusercontent.com/u/546312?v=4", "profile": "https://keybase.io/lambdalisue", "contributions": [ "bug" ] }, { "login": "bigshans", "name": "bigshans", "avatar_url": "https://avatars.githubusercontent.com/u/26884666?v=4", "profile": "http://bigshans.github.io", "contributions": [ "doc" ] }, { "login": "rob-3", "name": "Robert Boyd III", "avatar_url": "https://avatars.githubusercontent.com/u/24816247?v=4", "profile": "https://github.com/rob-3", "contributions": [ "bug" ] }, { "login": "creasty", "name": "Yuki Iwanaga", "avatar_url": "https://avatars.githubusercontent.com/u/1695538?v=4", "profile": "https://creasty.com", "contributions": [ "code" ] }, { "login": "springhack", "name": "SpringHack", "avatar_url": "https://avatars.githubusercontent.com/u/2389889?v=4", "profile": "https://www.dosk.win/", "contributions": [ "bug" ] }, { "login": "lmburns", "name": "Lucas Burns", "avatar_url": "https://avatars.githubusercontent.com/u/44355502?v=4", "profile": "http://git.lmburns.com", "contributions": [ "doc" ] }, { "login": "qiqiboy", "name": "qiqiboy", "avatar_url": "https://avatars.githubusercontent.com/u/3774036?v=4", "profile": "http://qiqi.boy.im", "contributions": [ "code" ] }, { "login": "timsu92", "name": "timsu92", "avatar_url": "https://avatars.githubusercontent.com/u/33785401?v=4", "profile": "https://github.com/timsu92", "contributions": [ "doc" ] }, { "login": "sartak", "name": "Shawn M Moore", "avatar_url": "https://avatars.githubusercontent.com/u/45430?v=4", "profile": "https://sartak.org", "contributions": [ "code" ] }, { "login": "aauren", "name": "Aaron U'Ren", "avatar_url": "https://avatars.githubusercontent.com/u/1392295?v=4", "profile": "https://github.com/aauren", "contributions": [ "bug" ] }, { "login": "SirCharlieMars", "name": "SeniorMars", "avatar_url": "https://avatars.githubusercontent.com/u/31679231?v=4", "profile": "https://github.com/SirCharlieMars", "contributions": [ "doc" ] }, { "login": "CollieIsCute", "name": "牧羊犬真Q", "avatar_url": "https://avatars.githubusercontent.com/u/43088530?v=4", "profile": "https://github.com/CollieIsCute", "contributions": [ "doc" ] }, { "login": "geraldspreer", "name": "geraldspreer", "avatar_url": "https://avatars.githubusercontent.com/u/1745692?v=4", "profile": "http://geraldspreer.com", "contributions": [ "doc" ] }, { "login": "3ximus", "name": "Fabio", "avatar_url": "https://avatars.githubusercontent.com/u/9083012?v=4", "profile": "http://3ximus.github.io/cv", "contributions": [ "doc" ] }, { "login": "skysky97", "name": "Li Yunting", "avatar_url": "https://avatars.githubusercontent.com/u/18086458?v=4", "profile": "https://github.com/skysky97", "contributions": [ "bug" ] }, { "login": "LebJe", "name": "Jeff L.", "avatar_url": "https://avatars.githubusercontent.com/u/51171427?v=4", "profile": "https://github.com/LebJe", "contributions": [ "code" ] }, { "login": "mcmire", "name": "Elliot Winkler", "avatar_url": "https://avatars.githubusercontent.com/u/7371?v=4", "profile": "https://hachyderm.io/@mcmire", "contributions": [ "code" ] }, { "login": "asmodeus812", "name": "Svetlozar Iliev", "avatar_url": "https://avatars.githubusercontent.com/u/15955811?v=4", "profile": "http://www.lebstertm.com", "contributions": [ "code" ] }, { "login": "43081j", "name": "James Garbutt", "avatar_url": "https://avatars.githubusercontent.com/u/5677153?v=4", "profile": "http://43081j.com/", "contributions": [ "code" ] }, { "login": "Kaiser-Yang", "name": "Qingzhou Yue", "avatar_url": "https://avatars.githubusercontent.com/u/58209855?v=4", "profile": "https://github.com/Kaiser-Yang", "contributions": [ "code" ] }, { "login": "de-vri-es", "name": "Maarten de Vries", "avatar_url": "https://avatars.githubusercontent.com/u/786213?v=4", "profile": "https://www.linkedin.com/in/de-vries-maarten/", "contributions": [ "code" ] }, { "login": "A4-Tacks", "name": "A4-Tacks", "avatar_url": "https://avatars.githubusercontent.com/u/102709083?v=4", "profile": "https://github.com/A4-Tacks", "contributions": [ "code" ] }, { "login": "zhixiao-zhang", "name": "forceofsystem", "avatar_url": "https://avatars.githubusercontent.com/u/89405463?v=4", "profile": "https://github.com/zhixiao-zhang", "contributions": [ "code" ] }, { "login": "statiolake", "name": "lake", "avatar_url": "https://avatars.githubusercontent.com/u/20490597?v=4", "profile": "https://github.com/statiolake", "contributions": [ "code" ] }, { "login": "davidosomething", "name": "David O'Trakoun", "avatar_url": "https://avatars.githubusercontent.com/u/609213?v=4", "profile": "https://www.davidosomething.com/", "contributions": [ "doc" ] }, { "login": "aispeaking", "name": "aispeaking", "avatar_url": "https://avatars.githubusercontent.com/u/139532597?v=4", "profile": "https://github.com/aispeaking", "contributions": [ "code" ] }, { "login": "cclauss", "name": "Christian Clauss", "avatar_url": "https://avatars.githubusercontent.com/u/3709715?v=4", "profile": "https://github.com/cclauss", "contributions": [ "code" ] }, { "login": "mehalter", "name": "Micah Halter", "avatar_url": "https://avatars.githubusercontent.com/u/1591837?v=4", "profile": "http://mehalter.com", "contributions": [ "code" ] }, { "login": "cridemichel", "name": "Cristiano De Michele", "avatar_url": "https://avatars.githubusercontent.com/u/15322138?v=4", "profile": "https://github.com/cridemichel", "contributions": [ "code" ] }, { "login": "YongJieYongJie", "name": "Yong Jie", "avatar_url": "https://avatars.githubusercontent.com/u/14101781?v=4", "profile": "https://yongjie.codes/", "contributions": [ "code" ] }, { "login": "hackergrrl", "name": "Kira Oakley", "avatar_url": "https://avatars.githubusercontent.com/u/489362?v=4", "profile": "http://eight45.net", "contributions": [ "doc" ] }, { "login": "merwan", "name": "Merouane Atig", "avatar_url": "https://avatars.githubusercontent.com/u/222879?v=4", "profile": "https://merwan.github.io", "contributions": [ "doc" ] }, { "login": "gera2ld", "name": "Gerald", "avatar_url": "https://avatars.githubusercontent.com/u/3139113?v=4", "profile": "https://gera2ld.space/", "contributions": [ "code" ] }, { "login": "V-Mann-Nick", "name": "Nicklas Sedlock", "avatar_url": "https://avatars.githubusercontent.com/u/47660390?v=4", "profile": "https://nicklas.sedlock.xyz/", "contributions": [ "code" ] }, { "login": "tcx4c70", "name": "Adam Tao", "avatar_url": "https://avatars.githubusercontent.com/u/16728230?v=4", "profile": "https://github.com/tcx4c70", "contributions": [ "code" ] }, { "login": "itsf4llofstars", "name": "itsf4llofstars", "avatar_url": "https://avatars.githubusercontent.com/u/90528743?v=4", "profile": "https://github.com/itsf4llofstars", "contributions": [ "doc" ] }, { "login": "brainwo", "name": "Brian Wo", "avatar_url": "https://avatars.githubusercontent.com/u/45139213?v=4", "profile": "https://github.com/brainwo", "contributions": [ "doc" ] }, { "login": "wsdjeg", "name": "Eric Wong", "avatar_url": "https://avatars.githubusercontent.com/u/13142418?v=4", "profile": "https://wsdjeg.net/", "contributions": [ "code" ] }, { "login": "oxalica", "name": "oxalica", "avatar_url": "https://avatars.githubusercontent.com/u/14816024?v=4", "profile": "https://github.com/oxalica", "contributions": [ "code" ] }, { "login": "laktak", "name": "Christian Zangl", "avatar_url": "https://avatars.githubusercontent.com/u/959858?v=4", "profile": "https://github.com/laktak", "contributions": [ "code" ] }, { "login": "zoumi", "name": "zoumi", "avatar_url": "https://avatars.githubusercontent.com/u/5162901?v=4", "profile": "https://github.com/zoumi", "contributions": [ "code" ] }, { "login": "atitcreate", "name": "atitcreate", "avatar_url": "https://avatars.githubusercontent.com/u/40348360?v=4", "profile": "https://github.com/atitcreate", "contributions": [ "code" ] } ], "contributorsPerLine": 7, "skipCi": true, "commitType": "docs" } ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf charset = utf-8 [*.{js,ts}] indent_style = space indent_size = 2 trim_trailing_whitespace = true max_line_length = 120 [*.json] indent_style = space indent_size = 2 trim_trailing_whitespace = true [*.vim] indent_style = space indent_size = 2 trim_trailing_whitespace = true max_line_length = 120 [*.lua] indent_size = 2 max_line_length = 120 align_call_args = only_not_exist_cross_row_expression align_table_field_to_first_field = true local_assign_continuation_align_to_first_expression = true keep_one_space_between_table_and_bracket = false quote_style = single remove_empty_header_and_footer_lines_in_function = true remove_expression_list_finish_comma= true ================================================ FILE: .github/.codecov.yml ================================================ coverage: status: patch: off ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms open_collective: cocnvim patreon: chemzqm ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- ## Result from CocInfo ## Describe the bug A clear and concise description of what the bug is. ## Reproduce the bug **We will close your issue when you don't provide minimal vimrc and we can't reproduce it** - Create file `mini.vim` with: ```vim set nocompatible set runtimepath^=/path/to/coc.nvim filetype plugin indent on syntax on set hidden ``` - Start (neo)vim with command: `vim -u mini.vim` - Operate vim. ## Screenshots (optional) If applicable, add screenshots to help explain your problem. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ci.yml ================================================ name: Dev on: push: branches: - master pull_request: branches: - master jobs: test: if: github.event.pull_request.draft == false timeout-minutes: 60 runs-on: ubuntu-latest strategy: fail-fast: false matrix: versions: - neovim: "stable" vim: "v9.0.0438" - neovim: "nightly" vim: "v9.1.1365" node: - "20" include: # only enable coverage on the fastest job - node: "20" ENABLE_CODE_COVERAGE: true env: NODE_ENV: test steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 2 - name: Setup Node.js ${{ matrix.node }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: "npm" - name: Setup python3 uses: actions/setup-python@v5 with: python-version: "3.x" - run: pip install pynvim - name: Setup vim uses: rhysd/action-setup-vim@v1 id: vim with: version: ${{ matrix.versions.vim }} - name: Setup neovim id: nvim uses: rhysd/action-setup-vim@v1 with: neovim: true version: ${{ matrix.versions.neovim }} - name: Install Dependencies run: | npm i -g bytes npm ci sudo apt-get install -y ripgrep exuberant-ctags rg --version ctags --version vim --version nvim --version - name: Run jest env: VIM_COMMAND: ${{ steps.vim.outputs.executable }} NVIM_COMMAND: ${{ steps.nvim.outputs.executable }} run: | node --max-old-space-size=4096 --expose-gc ./node_modules/.bin/jest --maxWorkers=2 --coverage --forceExit - name: Codecov uses: codecov/codecov-action@v4 if: ${{ matrix.ENABLE_CODE_COVERAGE }} with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false verbose: true ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - master pull_request: branches: - master jobs: lint: if: github.event.pull_request.draft == false name: Lint runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: cache: "npm" - name: Install Dependencies run: npm install --frozen-lockfile - name: Check Types by TSC run: npm run lint:typecheck - name: Lint ESLint run: npm run lint ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish Release Task on: schedule: - cron: '0 16 * * *' # UTC时间16:00(对应北京时间+8时区的0点) jobs: publish-release: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: master token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: true - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Setup python3 uses: actions/setup-python@v5 with: python-version: "3.x" - run: pip install pynvim - name: Setup vim uses: rhysd/action-setup-vim@v1 id: vim with: version: v9.0.0815 - name: Setup neovim id: nvim uses: rhysd/action-setup-vim@v1 with: neovim: true version: stable - name: Install Dependencies env: VIM_COMMAND: ${{ steps.vim.outputs.executable }} NVIM_COMMAND: ${{ steps.nvim.outputs.executable }} run: | npm i -g bytes npm ci NODE_ENV=production node esbuild.js sudo apt-get install -y ripgrep exuberant-ctags rg --version ctags --version vim --version nvim --version - name: Execute release.sh run: | chmod +x ./release.sh ./release.sh ================================================ FILE: .gitignore ================================================ lib .cache *.map coverage __pycache__ .pyc .log build doc/tags typings/package.json node_modules publish.sh !src/__tests__/tags src/__tests__/extensions/db.json ================================================ FILE: .ignore ================================================ lib ================================================ FILE: .npmignore ================================================ *.map .cache lib/extensions lib/__tests__ plugin autoload rplugin src .github build coverage data tslint.json tsconfig.json .zip .DS_Store ================================================ FILE: .prettierignore ================================================ src/ ================================================ FILE: .prettierrc ================================================ { "bracketSpacing": false, "arrowParens": "avoid", "printWidth": 120, "singleQuote": true, "trailingComma": "none", "tabWidth": 2, "proseWrap": "never", "semi": false } ================================================ FILE: .swcrc ================================================ { "sourceMaps": false, "module": { "type": "es6" }, "env": { "targets": { "node": "14" } }, "jsc": { "parser": { "syntax": "typescript", "tsx": false, "dynamicImport": false, "decorators": false }, "loose": true } } ================================================ FILE: .vim/coc-settings.json ================================================ { "eslint.validate": ["typescript"], "eslint.lintTask.options": ["."], "sumneko-lua.enableNvimLuaDev": true, "javascript.format.semicolons": "remove", "typescript.format.semicolons": "remove", "typescript.preferences.importModuleSpecifier": "relative", "typescript.preferences.importModuleSpecifierEnding": "minimal", "typescript.preferences.quoteStyle": "single", "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true, "Lua.diagnostics.disable": [ "empty-block" ] } ================================================ FILE: Backers.md ================================================ # Backers ❤️ coc.nvim? Help us keep it alive by [donating funds](https://www.bountysource.com/teams/coc-nvim)😘! oblitum free-easy ruanyl robjuffermans iamcco phcerdan sarene robtrac raidou tomspeak taigacute weirongxu tbo darthShadow yatli Matt Greer malob Emigre OkanEsen Lennaert Meijvogel Nils Landt dlants RCVU yatli mikker Velovix stCarolas Robbie Clarken hallettj appelgriebsch cosminadrianpopescu partizan ksaldana1 jesperryom JackCA peymanmortazavi jonaustin Yuriy Ivanyuk abenz1267 Sh3Rm4n mwcz Philipp-M gvelchuru JSamir toby de havilland viniciusarcanjo Mike Hearn darsto pyrho Frydac gsa9 _andys8 iago-lito ddaletski jonatan-branting yutakatay kevinrambaud tomaskallup LewisSteele ## 微信扫码赞助者 - free-easy - sarene - tomspeak - robtrac - 葫芦小金刚 - leo 陶 - 飞翔的白斩鸡 - mark_ll - 火冷 - Solomon - 李宇星 - Yus - IndexXuan - Sniper - 陈达野 - 胖听 - Jimmy - lightxue - 小亦俊 - 周慎敏 - 凤鸣 - Wilson - Abel ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## How do I... - [Use This Guide](#introduction)? - Make Something? 🤓👩🏽‍💻📜🍳 - [Project Setup](#project-setup) - [Contribute Documentation](#contribute-documentation) - [Contribute Code](#contribute-code) - Manage Something ✅🙆🏼💃👔 - [Provide Support on Issues](#provide-support-on-issues) - [Review Pull Requests](#review-pull-requests) - [Join the Project Team](#join-the-project-team) ## Introduction Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝 The [Project Team](#join-the-project-team) looks forward to your contributions. 🙌🏾✨ ## Project Setup So you wanna contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before. If this seems like a lot or you aren't able to do all this setup, you might also be able to [edit the files directly](https://help.github.com/articles/editing-files-in-another-user-s-repository/) without having to do any of this setup. Yes, [even code](#contribute-code). If you want to go the usual route and run the project locally, though: - [Install Node.js](https://nodejs.org/en/download/) - [Fork the project](https://guides.github.com/activities/forking/#fork) Then in your terminal: - Add coc.nvim to your vim's rtp by `set runtimepath^=/path/to/coc.nvim` - `cd path/to/your/coc.nvim` - `npm install` - Install [coc-tsserver](https://github.com/neoclide/coc-tsserver) by `:CocInstall coc-tsserver` in your vim - Install [coc-eslint](https://github.com/neoclide/coc-eslint) by `:CocInstall coc-eslint` in your vim. And you should be ready to go! ## Contribute Documentation Documentation is a super important, critical part of this project. Docs are how we keep track of what we're doing, how, and why. It's how we stay on the same page about our policies. And it's how we tell others everything they need in order to be able to use this project -- or contribute to it. So thank you in advance. Documentation contributions of any size are welcome! Feel free to file a PR even if you're just rewording a sentence to be more clear, or fixing a spelling mistake! To contribute documentation: - [Set up the project](#project-setup). - Edit or add any relevant documentation. - Make sure your changes are formatted correctly and consistently with the rest of the documentation. - Re-read what you wrote, and run a spellchecker on it to make sure you didn't miss anything. - In your commit message(s), begin the first line with `docs:`. For example: `docs: Adding a doc contrib section to CONTRIBUTING.md`. - Write clear, concise commit message(s) using [conventional-changelog format](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md). Documentation commits should use `docs(): `. - Go to https://github.com/neoclide/coc.nvim/pulls and open a new pull request with your changes. - If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing. ## Contribute Code We like code commits a lot! They're super handy, and they keep the project going and doing the work it needs to do to be useful to others. Code contributions of just about any size are acceptable! The main difference between code contributions and documentation contributions is that contributing code requires inclusion of relevant tests for the code being added or changed. Contributions without accompanying tests will be held off until a test is added, unless the maintainers consider the specific tests to be either impossible, or way too much of a burden for such a contribution. To contribute code: - [Set up the project](#project-setup). - Make any necessary changes to the source code. - Include any [additional documentation](#contribute-documentation) the changes might need. - Make sure the code doesn't have lint issue by command `npm run lint` in your terminal. - Write tests that verify that your contribution works as expected when necessary. - Make sure all tests passed by command `npm test` in your terminal. - Write clear, concise commit message(s) using [conventional-changelog format](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md). - Dependency updates, additions, or removals must be in individual commits, and the message must use the format: `(deps): PKG@VERSION`, where `` is any of the usual `conventional-changelog` prefixes, at your discretion. - Go to https://github.com/neoclide/coc.nvim/pulls and open a new pull request with your changes. - If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing. Once you've filed the PR: - Barring special circumstances, maintainers will not review PRs until all checks pass (Travis, AppVeyor, etc). - One or more maintainers will use GitHub's review feature to review your PR. - If the maintainer asks for any changes, edit your changes, push, and ask for another review. Additional tags (such as `needs-tests`) will be added depending on the review. - If the maintainer decides not to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. Please don't feel offended. We still really appreciate you taking the time to do it, and we don't take that lightly. 💚 - If your PR gets accepted, it will be marked as such, and merged into the `latest` branch soon after. Your contribution will be distributed to the masses next time the maintainers tag a release ## Provide Support on Issues [Needs Collaborator](#join-the-project-team): none Helping out other users with their questions is a really awesome way of contributing to any community. It's not uncommon for most of the issues on an open source projects being support-related questions by users trying to understand something they ran into, or find their way around a known bug. Sometimes, the `support` label will be added to things that turn out to actually be other things, like bugs or feature requests. In that case, suss out the details with the person who filed the original issue, add a comment explaining what the bug is, and change the label from `support` to `bug` or `feature`. If you can't do this yourself, @mention a maintainer so they can do it. In order to help other folks out with their questions: - Go to the issue tracker and [filter open issues by the `support` label](https://github.com/neoclide/coc.nvim/issues?q=is%3Aopen+is%3Aissue+label%3Asupport). - Read through the list until you find something that you're familiar enough with to give an answer to. - Respond to the issue with whatever details are needed to clarify the question, or get more details about what's going on. - Once the discussion wraps up and things are clarified, either close the issue, or ask the original issue filer (or a maintainer) to close it for you. Some notes on picking up support issues: - Avoid responding to issues you don't know you can answer accurately. - As much as possible, try to refer to past issues with accepted answers. Link to them from your replies with the `#123` format. - Be kind and patient with users -- often, folks who have run into confusing things might be upset or impatient. This is ok. Try to understand where they're coming from, and if you're too uncomfortable with the tone, feel free to stay away or withdraw from the issue. (note: if the user is outright hostile or is violating the CoC, [refer to the Code of Conduct](CODE_OF_CONDUCT.md) to resolve the conflict). ## Review Pull Requests [Needs Collaborator](#join-the-project-team): Issue Tracker While anyone can comment on a PR, add feedback, etc, PRs are only _approved_ by team members with Issue Tracker or higher permissions. PR reviews use [GitHub's own review feature](https://help.github.com/articles/about-pull-request-reviews/), which manages comments, approval, and review iteration. Some notes: - You may ask for minor changes ("nitpicks"), but consider whether they are really blockers to merging: try to err on the side of "approve, with comments". - _ALL PULL REQUESTS_ should be covered by a test: either by a previously-failing test, an existing test that covers the entire functionality of the submitted code, or new tests to verify any new/changed behavior. All tests must also pass and follow established conventions. Test coverage should not drop, unless the specific case is considered reasonable by maintainers. - Please make sure you're familiar with the code or documentation being updated, unless it's a minor change (spellchecking, minor formatting, etc). You may @mention another project member who you think is better suited for the review, but still provide a non-approving review of your own. - Be extra kind: people who submit code/doc contributions are putting themselves in a pretty vulnerable position, and have put time and care into what they've done (even if that's not obvious to you!) -- always respond with respect, be understanding, but don't feel like you need to sacrifice your standards for their sake, either. Just don't be a jerk about it? ## Join the Project Team ### Ways to Join There are many ways to contribute! Most of them don't require any official status unless otherwise noted. That said, there's a couple of positions that grant special repository abilities, and this section describes how they're granted and what they do. All of the below positions are granted based on the project team's needs, as well as their consensus opinion about whether they would like to work with the person and think that they would fit well into that position. The process is relatively informal, and it's likely that people who express interest in participating can just be granted the permissions they'd like. You can spot a collaborator on the repo by looking for the `[Collaborator]` or `[Owner]` tags next to their names. | Permission | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Issue Tracker | Granted to contributors who express a strong interest in spending time on the project's issue tracker. These tasks are mainly labeling issues, cleaning up old ones, and [reviewing pull requests](#review-pull-requests), as well as all the usual things non-team-member contributors can do. Issue handlers should not merge pull requests, tag releases, or directly commit code themselves: that should still be done through the usual pull request process. Becoming an Issue Handler means the project team trusts you to understand enough of the team's process and context to implement it on the issue tracker. | | Committer | Granted to contributors who want to handle the actual pull request merges, tagging new versions, etc. Committers should have a good level of familiarity with the codebase, and enough context to understand the implications of various changes, as well as a good sense of the will and expectations of the project team. | | Admin/Owner | Granted to people ultimately responsible for the project, its community, etc. | ================================================ FILE: LICENSE.md ================================================ Copyright (c) <2022> "Anti 996" License Version 1.0 (Draft) Permission is hereby granted to any individual or legal entity obtaining a copy of this licensed work (including the source code, documentation and/or related items, hereinafter collectively referred to as the "licensed work"), free of charge, to deal with the licensed work for any purpose, including without limitation, the rights to use, reproduce, modify, prepare derivative works of, distribute, publish and sublicense the licensed work, subject to the following conditions: 1. The individual or the legal entity must conspicuously display, without modification, this License and the notice on each redistributed or derivative copy of the Licensed Work. 2. The individual or the legal entity must strictly comply with all applicable laws, regulations, rules and standards of the jurisdiction relating to labor and employment where the individual is physically located or where the individual was born or naturalized; or where the legal entity is registered or is operating (whichever is stricter). In case that the jurisdiction has no such laws, regulations, rules and standards or its laws, regulations, rules and standards are unenforceable, the individual or the legal entity are required to comply with Core International Labor Standards. 3. The individual or the legal entity shall not induce, suggest or force its employee(s), whether full-time or part-time, or its independent contractor(s), in any methods, to agree in oral or written form, to directly or indirectly restrict, weaken or relinquish his or her rights or remedies under such laws, regulations, rules and standards relating to labor and employment as mentioned above, no matter whether such written or oral agreements are enforceable under the laws of the said jurisdiction, nor shall such individual or the legal entity limit, in any methods, the rights of its employee(s) or independent contractor(s) from reporting or complaining to the copyright holder or relevant authorities monitoring the compliance of the license about its violation(s) of the said license. THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. ================================================ FILE: README.md ================================================

Logo

Make your Vim/Neovim as smart as VS Code

Software License Actions Codecov Coverage Status Doc Ask DeepWiki

--- Custom coc popup menu with snippet support _Custom popup menu with snippet support_ ## Why? - 🚀 **Fast**: separated NodeJS process that does not slow down Vim most of the time. - 💎 **Reliable**: typed language, tested with CI. - 🌟 **Featured**: all LSP 3.16 features are supported, see `:h coc-lsp`. - ❤️ **Flexible**: [configured like VS Code](https://github.com/neoclide/coc.nvim/wiki/Using-the-configuration-file), [Coc extensions function similarly to VS Code extensions](https://github.com/neoclide/coc.nvim/wiki/Using-coc-extensions) ## Quick Start Make sure use Vim >= 9.0.0438 or Neovim >= 0.8.0. Install [nodejs](https://nodejs.org/en/download/) >= 16.18.0: ```bash curl -sL install-node.vercel.app/lts | bash ``` For [vim-plug](https://github.com/junegunn/vim-plug) users: ```vim " Use release branch (recommended) Plug 'neoclide/coc.nvim', {'branch': 'release'} " Or build from source code by using npm Plug 'neoclide/coc.nvim', {'branch': 'master', 'do': 'npm ci'} ``` in your `.vimrc` or `init.vim`, then restart Vim and run `:PlugInstall`. Checkout [Install coc.nvim](https://github.com/neoclide/coc.nvim/wiki/Install-coc.nvim) for more info. You **have to** install coc extensions or configure language servers for LSP support. Install extensions like this: :CocInstall coc-json coc-tsserver Or you can configure a language server in your `coc-settings.json`(open it using `:CocConfig`) like this: ```json { "languageserver": { "go": { "command": "gopls", "rootPatterns": ["go.mod"], "trace.server": "verbose", "filetypes": ["go"] } } } ``` Checkout the wiki for more details: - [Completion with sources](https://github.com/neoclide/coc.nvim/wiki/Completion-with-sources) - [Using the configuration file](https://github.com/neoclide/coc.nvim/wiki/Using-the-configuration-file) - [Using coc extensions](https://github.com/neoclide/coc.nvim/wiki/Using-coc-extensions) - [Configure language servers](https://github.com/neoclide/coc.nvim/wiki/Language-servers) - [F.A.Q](https://github.com/neoclide/coc.nvim/wiki/F.A.Q) Checkout `:h coc-nvim` for Vim interface. ## Example Vim configuration Configuration is required to make coc.nvim easier to work with, since it doesn't change your key-mappings or Vim options. This is done as much as possible to avoid conflict with your other plugins. **❗️Important**: Some Vim plugins can change your key mappings. Please use command like`:verbose imap ` to make sure that your keymap has taken effect. ```vim " https://raw.githubusercontent.com/neoclide/coc.nvim/master/doc/coc-example-config.vim " May need for Vim (not Neovim) since coc.nvim calculates byte offset by count " utf-8 byte sequence set encoding=utf-8 " Some servers have issues with backup files, see #649 set nobackup set nowritebackup " Having longer updatetime (default is 4000 ms = 4s) leads to noticeable " delays and poor user experience set updatetime=300 " Always show the signcolumn, otherwise it would shift the text each time " diagnostics appear/become resolved set signcolumn=yes " Use tab for trigger completion with characters ahead and navigate " NOTE: There's always complete item selected by default, you may want to enable " no select by `"suggest.noselect": true` in your configuration file " NOTE: Use command ':verbose imap ' to make sure tab is not mapped by " other plugin before putting this into your config inoremap \ coc#pum#visible() ? coc#pum#next(1) : \ CheckBackspace() ? "\" : \ coc#refresh() inoremap coc#pum#visible() ? coc#pum#prev(1) : "\" " Make to accept selected completion item or notify coc.nvim to format " u breaks current undo, please make your own choice inoremap coc#pum#visible() ? coc#pum#confirm() \: "\u\\=coc#on_enter()\" function! CheckBackspace() abort let col = col('.') - 1 return !col || getline('.')[col - 1] =~# '\s' endfunction " Use to trigger completion if has('nvim') inoremap coc#refresh() else inoremap coc#refresh() endif " Use `[g` and `]g` to navigate diagnostics " Use `:CocDiagnostics` to get all diagnostics of current buffer in location list nmap [g (coc-diagnostic-prev) nmap ]g (coc-diagnostic-next) " GoTo code navigation nmap gd (coc-definition) nmap gy (coc-type-definition) nmap gi (coc-implementation) nmap gr (coc-references) " Use K to show documentation in preview window nnoremap K :call ShowDocumentation() function! ShowDocumentation() if CocAction('hasProvider', 'hover') call CocActionAsync('doHover') else call feedkeys('K', 'in') endif endfunction " Highlight the symbol and its references when holding the cursor autocmd CursorHold * silent call CocActionAsync('highlight') " Symbol renaming nmap rn (coc-rename) " Formatting selected code xmap f (coc-format-selected) nmap f (coc-format-selected) augroup mygroup autocmd! " Setup formatexpr specified filetype(s) autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected') augroup end " Applying code actions to the selected code block " Example: `aap` for current paragraph xmap a (coc-codeaction-selected) nmap a (coc-codeaction-selected) " Remap keys for applying code actions at the cursor position nmap ac (coc-codeaction-cursor) " Remap keys for apply code actions affect whole buffer nmap as (coc-codeaction-source) " Apply the most preferred quickfix action to fix diagnostic on the current line nmap qf (coc-fix-current) " Remap keys for applying refactor code actions nmap re (coc-codeaction-refactor) xmap r (coc-codeaction-refactor-selected) nmap r (coc-codeaction-refactor-selected) " Run the Code Lens action on the current line nmap cl (coc-codelens-action) " Map function and class text objects " NOTE: Requires 'textDocument.documentSymbol' support from the language server xmap if (coc-funcobj-i) omap if (coc-funcobj-i) xmap af (coc-funcobj-a) omap af (coc-funcobj-a) xmap ic (coc-classobj-i) omap ic (coc-classobj-i) xmap ac (coc-classobj-a) omap ac (coc-classobj-a) " Remap and to scroll float windows/popups if has('nvim-0.4.0') || has('patch-8.2.0750') nnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\" nnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\" inoremap coc#float#has_scroll() ? "\=coc#float#scroll(1)\" : "\" inoremap coc#float#has_scroll() ? "\=coc#float#scroll(0)\" : "\" vnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\" vnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\" endif " Use CTRL-S for selections ranges " Requires 'textDocument/selectionRange' support of language server nmap (coc-range-select) xmap (coc-range-select) " Add `:Format` command to format current buffer command! -nargs=0 Format :call CocActionAsync('format') " Add `:Fold` command to fold current buffer command! -nargs=? Fold :call CocAction('fold', ) " Add `:OR` command for organize imports of the current buffer command! -nargs=0 OR :call CocActionAsync('runCommand', 'editor.action.organizeImport') " Add (Neo)Vim's native statusline support " NOTE: Please see `:h coc-status` for integrations with external plugins that " provide custom statusline: lightline.vim, vim-airline set statusline^=%{coc#status()}%{get(b:,'coc_current_function','')} " Mappings for CoCList " Show all diagnostics nnoremap a :CocList diagnostics " Manage extensions nnoremap e :CocList extensions " Show commands nnoremap c :CocList commands " Find symbol of current document nnoremap o :CocList outline " Search workspace symbols nnoremap s :CocList -I symbols " Do default action for next item nnoremap j :CocNext " Do default action for previous item nnoremap k :CocPrev " Resume latest coc list nnoremap p :CocListResume ``` ## Example Lua configuration NOTE: This only works in Neovim 0.7.0dev+. ```lua -- https://raw.githubusercontent.com/neoclide/coc.nvim/master/doc/coc-example-config.lua -- Some servers have issues with backup files, see #649 vim.opt.backup = false vim.opt.writebackup = false -- Having longer updatetime (default is 4000 ms = 4s) leads to noticeable -- delays and poor user experience vim.opt.updatetime = 300 -- Always show the signcolumn, otherwise it would shift the text each time -- diagnostics appeared/became resolved vim.opt.signcolumn = "yes" local keyset = vim.keymap.set -- Autocomplete function _G.check_back_space() local col = vim.fn.col('.') - 1 return col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') ~= nil end -- Use Tab for trigger completion with characters ahead and navigate -- NOTE: There's always a completion item selected by default, you may want to enable -- no select by setting `"suggest.noselect": true` in your configuration file -- NOTE: Use command ':verbose imap ' to make sure Tab is not mapped by -- other plugins before putting this into your config local opts = {silent = true, noremap = true, expr = true, replace_keycodes = false} keyset("i", "", 'coc#pum#visible() ? coc#pum#next(1) : v:lua.check_back_space() ? "" : coc#refresh()', opts) keyset("i", "", [[coc#pum#visible() ? coc#pum#prev(1) : "\"]], opts) -- Make to accept selected completion item or notify coc.nvim to format -- u breaks current undo, please make your own choice keyset("i", "", [[coc#pum#visible() ? coc#pum#confirm() : "\u\\=coc#on_enter()\"]], opts) -- Use to trigger snippets keyset("i", "", "(coc-snippets-expand-jump)") -- Use to trigger completion keyset("i", "", "coc#refresh()", {silent = true, expr = true}) -- Use `[g` and `]g` to navigate diagnostics -- Use `:CocDiagnostics` to get all diagnostics of current buffer in location list keyset("n", "[g", "(coc-diagnostic-prev)", {silent = true}) keyset("n", "]g", "(coc-diagnostic-next)", {silent = true}) -- GoTo code navigation keyset("n", "gd", "(coc-definition)", {silent = true}) keyset("n", "gy", "(coc-type-definition)", {silent = true}) keyset("n", "gi", "(coc-implementation)", {silent = true}) keyset("n", "gr", "(coc-references)", {silent = true}) -- Use K to show documentation in preview window function _G.show_docs() local cw = vim.fn.expand('') if vim.fn.index({'vim', 'help'}, vim.bo.filetype) >= 0 then vim.api.nvim_command('h ' .. cw) elseif vim.api.nvim_eval('coc#rpc#ready()') then vim.fn.CocActionAsync('doHover') else vim.api.nvim_command('!' .. vim.o.keywordprg .. ' ' .. cw) end end keyset("n", "K", 'lua _G.show_docs()', {silent = true}) -- Highlight the symbol and its references on a CursorHold event(cursor is idle) vim.api.nvim_create_augroup("CocGroup", {}) vim.api.nvim_create_autocmd("CursorHold", { group = "CocGroup", command = "silent call CocActionAsync('highlight')", desc = "Highlight symbol under cursor on CursorHold" }) -- Symbol renaming keyset("n", "rn", "(coc-rename)", {silent = true}) -- Formatting selected code keyset("x", "f", "(coc-format-selected)", {silent = true}) keyset("n", "f", "(coc-format-selected)", {silent = true}) -- Setup formatexpr specified filetype(s) vim.api.nvim_create_autocmd("FileType", { group = "CocGroup", pattern = "typescript,json", command = "setl formatexpr=CocAction('formatSelected')", desc = "Setup formatexpr specified filetype(s)." }) -- Apply codeAction to the selected region -- Example: `aap` for current paragraph local opts = {silent = true, nowait = true} keyset("x", "a", "(coc-codeaction-selected)", opts) keyset("n", "a", "(coc-codeaction-selected)", opts) -- Remap keys for apply code actions at the cursor position. keyset("n", "ac", "(coc-codeaction-cursor)", opts) -- Remap keys for apply source code actions for current file. keyset("n", "as", "(coc-codeaction-source)", opts) -- Apply the most preferred quickfix action on the current line. keyset("n", "qf", "(coc-fix-current)", opts) -- Remap keys for apply refactor code actions. keyset("n", "re", "(coc-codeaction-refactor)", { silent = true }) keyset("x", "r", "(coc-codeaction-refactor-selected)", { silent = true }) keyset("n", "r", "(coc-codeaction-refactor-selected)", { silent = true }) -- Run the Code Lens actions on the current line keyset("n", "cl", "(coc-codelens-action)", opts) -- Map function and class text objects -- NOTE: Requires 'textDocument.documentSymbol' support from the language server keyset("x", "if", "(coc-funcobj-i)", opts) keyset("o", "if", "(coc-funcobj-i)", opts) keyset("x", "af", "(coc-funcobj-a)", opts) keyset("o", "af", "(coc-funcobj-a)", opts) keyset("x", "ic", "(coc-classobj-i)", opts) keyset("o", "ic", "(coc-classobj-i)", opts) keyset("x", "ac", "(coc-classobj-a)", opts) keyset("o", "ac", "(coc-classobj-a)", opts) -- Remap and to scroll float windows/popups ---@diagnostic disable-next-line: redefined-local local opts = {silent = true, nowait = true, expr = true} keyset("n", "", 'coc#float#has_scroll() ? coc#float#scroll(1) : ""', opts) keyset("n", "", 'coc#float#has_scroll() ? coc#float#scroll(0) : ""', opts) keyset("i", "", 'coc#float#has_scroll() ? "=coc#float#scroll(1)" : ""', opts) keyset("i", "", 'coc#float#has_scroll() ? "=coc#float#scroll(0)" : ""', opts) keyset("v", "", 'coc#float#has_scroll() ? coc#float#scroll(1) : ""', opts) keyset("v", "", 'coc#float#has_scroll() ? coc#float#scroll(0) : ""', opts) -- Use CTRL-S for selections ranges -- Requires 'textDocument/selectionRange' support of language server keyset("n", "", "(coc-range-select)", {silent = true}) keyset("x", "", "(coc-range-select)", {silent = true}) -- Add `:Format` command to format current buffer vim.api.nvim_create_user_command("Format", "call CocAction('format')", {}) -- " Add `:Fold` command to fold current buffer vim.api.nvim_create_user_command("Fold", "call CocAction('fold', )", {nargs = '?'}) -- Add `:OR` command for organize imports of the current buffer vim.api.nvim_create_user_command("OR", "call CocActionAsync('runCommand', 'editor.action.organizeImport')", {}) -- Add (Neo)Vim's native statusline support -- NOTE: Please see `:h coc-status` for integrations with external plugins that -- provide custom statusline: lightline.vim, vim-airline vim.opt.statusline:prepend("%{coc#status()}%{get(b:,'coc_current_function','')}") -- Mappings for CoCList -- code actions and coc stuff ---@diagnostic disable-next-line: redefined-local local opts = {silent = true, nowait = true} -- Show all diagnostics keyset("n", "a", ":CocList diagnostics", opts) -- Manage extensions keyset("n", "e", ":CocList extensions", opts) -- Show commands keyset("n", "c", ":CocList commands", opts) -- Find symbol of current document keyset("n", "o", ":CocList outline", opts) -- Search workspace symbols keyset("n", "s", ":CocList -I symbols", opts) -- Do default action for next item keyset("n", "j", ":CocNext", opts) -- Do default action for previous item keyset("n", "k", ":CocPrev", opts) -- Resume latest coc list keyset("n", "p", ":CocListResume", opts) ``` ## Articles - [coc.nvim 插件体系介绍](https://zhuanlan.zhihu.com/p/65524706) - [CocList 入坑指南](https://zhuanlan.zhihu.com/p/71846145) - [Create coc.nvim extension to improve Vim experience](https://medium.com/@chemzqm/create-coc-nvim-extension-to-improve-vim-experience-4461df269173) - [How to write a coc.nvim extension (and why)](https://samroeca.com/coc-plugin.html) ## Troubleshooting Try these steps if you experience problems with coc.nvim: - Ensure your Vim version >= 8.0 using `:version` - If a service failed to start, use `:CocInfo` or `:checkhealth` if you use Neovim - Checkout the log of coc.nvim with `:CocOpenLog` - If you have issues with the language server, it's recommended to [checkout the language server output](https://github.com/neoclide/coc.nvim/wiki/Debug-language-server#using-output-channel) ## Feedback - Have a question? Start a discussion on [GitHub Discussions](https://github.com/neoclide/coc.nvim/discussions). - File a bug in [GitHub Issues](https://github.com/neoclide/coc.nvim/issues). ## Backers [Become a backer](https://opencollective.com/cocnvim#backer) and get your image on our README on GitHub with a link to your site. ## Contributors
Qiming zhao
Qiming zhao

💻
Heyward Fann
Heyward Fann

💻
Raidou
Raidou

💻
kevinhwang91
kevinhwang91

💻
年糕小豆汤
年糕小豆汤

💻
Avi Dessauer
Avi Dessauer

💻
最上川
最上川

💻
Yatao Li
Yatao Li

💻
wongxy
wongxy

💻
Sam McCall
Sam McCall

💻
Samuel Roeca
Samuel Roeca

💻
Amirali Esmaeili
Amirali Esmaeili

💻
Jack Rowlingson
Jack Rowlingson

💻
Jaehwang Jung
Jaehwang Jung

💻
Antoine
Antoine

💻
Cosmin Popescu
Cosmin Popescu

💻
Duc Nghiem Xuan
Duc Nghiem Xuan

💻
Francisco Lopes
Francisco Lopes

💻
daquexian
daquexian

💻
dependabot[bot]
dependabot[bot]

💻
greenkeeper[bot]
greenkeeper[bot]

💻
Chris Kipp
Chris Kipp

💻
Dmytro Meleshko
Dmytro Meleshko

💻
Kirill Bobyrev
Kirill Bobyrev

💻
Gontran Baerts
Gontran Baerts

💻
Andy
Andy

💻
Cheng JIANG
Cheng JIANG

💻
Corin
Corin

💻
Daniel Zhang
Daniel Zhang

💻
Ferdinand Bachmann
Ferdinand Bachmann

💻
Guangqing Chen
Guangqing Chen

💻
Jade Meskill
Jade Meskill

💻
Jasper Poppe
Jasper Poppe

💻
Jean Jordaan
Jean Jordaan

💻
Kid
Kid

💻
Pieter van Loon
Pieter van Loon

💻
Robert Liebowitz
Robert Liebowitz

💻
Seth Messer
Seth Messer

💻
UncleBill
UncleBill

💻
ZERO
ZERO

💻
fsouza
fsouza

💻
XiaoZhang
XiaoZhang

💻
whyreal
whyreal

💻
yehuohan
yehuohan

💻
バクダンくん
バクダンくん

💻
Raphael
Raphael

💻
tbodt
tbodt

💻
Aaron McDaid
Aaron McDaid

💻
Aasif Versi
Aasif Versi

💻
Abner Silva
Abner Silva

💻
Adam Stankiewicz
Adam Stankiewicz

💻
Adamansky Anton
Adamansky Anton

💻
Ahmed El Gabri
Ahmed El Gabri

💻
Alexandr Kondratev
Alexandr Kondratev

💻
Andrew Shim
Andrew Shim

💻
Andy Lindeman
Andy Lindeman

💻
Augustin
Augustin

💻
Bastien Orivel
Bastien Orivel

💻
Ben Lu
Ben Lu

💻
Ben
Ben

💻
Brendan Roy
Brendan Roy

💻
brianembry
brianembry

💻
br
br

💻
Cason Adams
Cason Adams

💻
Chang Y
Chang Y

💻
Chayoung You
Chayoung You

💻
Chen Lijun
Chen Lijun

💻
Chen Mulong
Chen Mulong

💻
Chris Weyl
Chris Weyl

💻
dezza
dezza

💻
Cody Allen
Cody Allen

💻
Damien Rajon
Damien Rajon

💻
Daniel Eriksson
Daniel Eriksson

💻
Daniel Jenson
Daniel Jenson

💻
David Mejorado
David Mejorado

💻
Deric Pang
Deric Pang

💻
Ding Tao
Ding Tao

💻
Doron Behar
Doron Behar

💻
Egor Kovetskiy
Egor Kovetskiy

💻
ElKowar
ElKowar

💻
Emeliov Dmitrii
Emeliov Dmitrii

💻
Fabian Becker
Fabian Becker

💻
FallenWarrior2k
FallenWarrior2k

💻
Fausto Núñez Alberro
Fausto Núñez Alberro

💻
Felipe Ramos
Felipe Ramos

💻
Fredrik Borg
Fredrik Borg

💻
Gavin Sim
Gavin Sim

💻
Gibson Fahnestock
Gibson Fahnestock

💻
Giovanni Giordano
Giovanni Giordano

💻
Gopal Adhikari
Gopal Adhikari

💻
Hanh Le
Hanh Le

💻
hedy
hedy

💻
Hendrik Lammers
Hendrik Lammers

💻
Henry Barreto
Henry Barreto

💻
Hugo
Hugo

💻
Jackie Li
Jackie Li

💻
Jakub Nowak
Jakub Nowak

💻
James Pickard
James Pickard

💻
Jia Sui
Jia Sui

💻
Ellie Hermaszewska
Ellie Hermaszewska

💻
Joel Bradshaw
Joel Bradshaw

💻
John Carlo Roberto
John Carlo Roberto

💻
Jonas Holst Damtoft
Jonas Holst Damtoft

💻
Jonathan Lehman
Jonathan Lehman

💻
Joosep Alviste
Joosep Alviste

💻
Josa Gesell
Josa Gesell

💻
Joshua Rubin
Joshua Rubin

💻
Julian Grinblat
Julian Grinblat

💻
Julian Valentin
Julian Valentin

💻
KabbAmine
KabbAmine

💻
Kay Gosho
Kay Gosho

💻
Kenny Huynh
Kenny Huynh

💻
Kevin Rambaud
Kevin Rambaud

💻
Kian Cross
Kian Cross

💻
Kristijan Husak
Kristijan Husak

💻
NullVoxPopuli
NullVoxPopuli

💻
Lasse Peters
Lasse Peters

💻
Noel Errenil
Noel Errenil

💻
LinArcX
LinArcX

💻
Liu-Cheng Xu
Liu-Cheng Xu

💻
Marc
Marc

💻
Marius Gawrisch
Marius Gawrisch

💻
Mark Hintz
Mark Hintz

💻
Mathieu Le Tiec
Mathieu Le Tiec

💻
Matt White
Matt White

💻
Matthew Evans
Matthew Evans

💻
Me1onRind
Me1onRind

💻
Qyriad
Qyriad

💻
Narcis B.
Narcis B.

💻
Neur1n
Neur1n

💻
Nicolas Dermine
Nicolas Dermine

💻
Noah
Noah

💻
PENG Rui
PENG Rui

💻
Paco
Paco

💻
Peng Guanwen
Peng Guanwen

💻
Petter Wahlman
Petter Wahlman

💻
Pooya Moradi
Pooya Moradi

💻
Quade Morrison
Quade Morrison

💻
Ralf Vogler
Ralf Vogler

💻
Ran Chen
Ran Chen

💻
Ricardo García Vega
Ricardo García Vega

💻
Rick Jones
Rick Jones

💻
Ryan Christian
Ryan Christian

💻
Salo
Salo

💻
Sam Nolan
Sam Nolan

💻
Saurav
Saurav

💻
Sean Mackesey
Sean Mackesey

💻
Sheel Patel
Sheel Patel

💻
Solomon Ng
Solomon Ng

💻
Sri Kadimisetty
Sri Kadimisetty

💻
Stephen Prater
Stephen Prater

💻
Sune Kibsgaard
Sune Kibsgaard

💻
Aquaakuma
Aquaakuma

💻
Takumi Kawase
Takumi Kawase

💻
The Blob SCP
The Blob SCP

💻
Tomasz N
Tomasz N

💻
Tomoyuki Harada
Tomoyuki Harada

💻
Tony Fettes
Tony Fettes

💻
Tony Narlock
Tony Narlock

💻
Tony Wang
Tony Wang

💻
Victor Quach
Victor Quach

💻
Whisperity
Whisperity

💻
William Turner
William Turner

💻
Xiaochao Dong
Xiaochao Dong

💻
Hugh Hou
Hugh Hou

💻
Jackie Li
Jackie Li

💻
Zachary Freed
Zachary Freed

💻
akiyosi
akiyosi

💻
alexjg
alexjg

💻
aste4
aste4

💻
clyfish
clyfish

💻
dev7ba
dev7ba

💻
diartyz
diartyz

💻
doza-daniel
doza-daniel

💻
equal-l2
equal-l2

💻
fong
fong

💻
hexh
hexh

💻
hhiraba
hhiraba

💻
ic-768
ic-768

💻
javiertury
javiertury

💻
karasu
karasu

💻
kevineato
kevineato

💻
Eduardo Costa
Eduardo Costa

💻
micchy326
micchy326

💻
midchildan
midchildan

💻
minefuto
minefuto

💻
miyanokomiya
miyanokomiya

💻
miyaviee
miyaviee

💻
monkoose
monkoose

💻 🐛
mujx
mujx

💻
mvilim
mvilim

💻
naruaway
naruaway

💻
piersy
piersy

💻
ryantig
ryantig

💻
rydesun
rydesun

💻
sc00ter
sc00ter

💻
smhc
smhc

💻
Sam Kaplan
Sam Kaplan

💻
tasuten
tasuten

💻
todesking
todesking

💻
typicode
typicode

💻
李鸣飞
李鸣飞

💻
Ikko Ashimine
Ikko Ashimine

📖
Rammiah
Rammiah

🐛
Alisue
Alisue

🐛
bigshans
bigshans

📖
Robert Boyd III
Robert Boyd III

🐛
Yuki Iwanaga
Yuki Iwanaga

💻
SpringHack
SpringHack

🐛
Lucas Burns
Lucas Burns

📖
qiqiboy
qiqiboy

💻
timsu92
timsu92

📖
Shawn M Moore
Shawn M Moore

💻
Aaron U'Ren
Aaron U'Ren

🐛
SeniorMars
SeniorMars

📖
牧羊犬真Q
牧羊犬真Q

📖
geraldspreer
geraldspreer

📖
Fabio
Fabio

📖
Li Yunting
Li Yunting

🐛
Jeff L.
Jeff L.

💻
Elliot Winkler
Elliot Winkler

💻
Svetlozar Iliev
Svetlozar Iliev

💻
James Garbutt
James Garbutt

💻
Qingzhou Yue
Qingzhou Yue

💻
Maarten de Vries
Maarten de Vries

💻
A4-Tacks
A4-Tacks

💻
forceofsystem
forceofsystem

💻
lake
lake

💻
David O'Trakoun
David O'Trakoun

📖
aispeaking
aispeaking

💻
Christian Clauss
Christian Clauss

💻
Micah Halter
Micah Halter

💻
Cristiano De Michele
Cristiano De Michele

💻
Yong Jie
Yong Jie

💻
Kira Oakley
Kira Oakley

📖
Merouane Atig
Merouane Atig

📖
Gerald
Gerald

💻
Nicklas Sedlock
Nicklas Sedlock

💻
Adam Tao
Adam Tao

💻
itsf4llofstars
itsf4llofstars

📖
Brian Wo
Brian Wo

📖
Eric Wong
Eric Wong

💻
oxalica
oxalica

💻
Christian Zangl
Christian Zangl

💻
zoumi
zoumi

💻
atitcreate
atitcreate

💻
This project follows the [all-contributors](https://allcontributors.org) specification. Contributions of any kind are welcome! ## License [Anti 996](./LICENSE.md) ================================================ FILE: autoload/coc/api.vim ================================================ if has('nvim') finish endif vim9script scriptencoding utf-8 var namespace_id: number = 1 final namespace_cache: dict = {} var max_src_id: number = 1000 # bufnr => max textprop id final buffer_id: dict = {} # srcId => list of types final id_types: dict = {} var tab_id: number = 1 final listener_map: dict = {} const prop_offset: number = get(g:, 'coc_text_prop_offset', 1000) const keymap_arguments: list = ['nowait', 'silent', 'script', 'expr', 'unique', 'special'] const known_types = ['Number', 'String', 'Funcref', 'List', 'Dictionary', 'Float', 'Boolean', 'None', 'Job', 'Channel', 'Blob'] const scopes = ['global', 'local'] # Boolean options of vim 9.1.1134 const boolean_options: list = ['allowrevins', 'arabic', 'arabicshape', 'autochdir', 'autoindent', 'autoread', 'autoshelldir', 'autowrite', 'autowriteall', 'backup', 'balloonevalterm', 'binary', 'bomb', 'breakindent', 'buflisted', 'cdhome', 'cindent', 'compatible', 'confirm', 'copyindent', 'cursorbind', 'cursorcolumn', 'cursorline', 'delcombine', 'diff', 'digraph', 'edcompatible', 'emoji', 'endoffile', 'endofline', 'equalalways', 'errorbells', 'esckeys', 'expandtab', 'exrc', 'fileignorecase', 'fixendofline', 'foldenable', 'fsync', 'gdefault', 'hidden', 'hkmap', 'hkmapp', 'hlsearch', 'icon', 'ignorecase', 'imcmdline', 'imdisable', 'incsearch', 'infercase', 'insertmode', 'joinspaces', 'langnoremap', 'langremap', 'lazyredraw', 'linebreak', 'lisp', 'list', 'loadplugins', 'magic', 'modeline', 'modelineexpr', 'modifiable', 'modified', 'more', 'number', 'paste', 'preserveindent', 'previewwindow', 'prompt', 'readonly', 'relativenumber', 'remap', 'revins', 'rightleft', 'ruler', 'scrollbind', 'secure', 'shelltemp', 'shiftround', 'shortname', 'showcmd', 'showfulltag', 'showmatch', 'showmode', 'smartcase', 'smartindent', 'smarttab', 'smoothscroll', 'spell', 'splitbelow', 'splitright', 'startofline', 'swapfile', 'tagbsearch', 'tagrelative', 'tagstack', 'termbidi', 'termguicolors', 'terse', 'textauto', 'textmode', 'tildeop', 'timeout', 'title', 'ttimeout', 'ttybuiltin', 'ttyfast', 'undofile', 'visualbell', 'warn', 'weirdinvert', 'wildignorecase', 'wildmenu', 'winfixbuf', 'winfixheight', 'winfixwidth', 'wrap', 'wrapscan', 'write', 'writeany', 'writebackup', 'xtermcodes'] const window_options = keys(getwinvar(0, '&')) const buffer_options = keys(getbufvar(bufnr('%'), '&')) var group_id: number = 1 # id => name final groups_map: dict = {} var autocmd_id: number = 1 final autocmds_map: dict = {} const API_FUNCTIONS = [ 'eval', 'command', 'feedkeys', 'command_output', 'exec', 'input', 'create_buf', 'strwidth', 'out_write', 'err_write', 'err_writeln', 'set_option', 'set_var', 'set_keymap', 'set_option_value', 'set_current_line', 'set_current_dir', 'set_current_buf', 'set_current_win', 'set_current_tabpage', 'get_option', 'get_api_info', 'get_current_line', 'get_var', 'get_vvar', 'get_current_buf', 'get_current_win', 'get_current_tabpage', 'get_mode', 'get_namespaces', 'get_option_value', 'del_var', 'del_keymap', 'del_current_line', 'del_autocmd', 'list_wins', 'list_bufs', 'list_runtime_paths', 'list_tabpages', 'call_atomic', 'call_function', 'call_dict_function', 'create_namespace', 'create_augroup', 'create_autocmd', 'buf_set_option', 'buf_get_option', 'buf_get_changedtick', 'buf_is_valid', 'buf_is_loaded', 'buf_get_mark', 'buf_add_highlight', 'buf_clear_namespace', 'buf_line_count', 'buf_attach', 'buf_detach', 'buf_get_lines', 'buf_set_lines', 'buf_set_name', 'buf_get_name', 'buf_get_var', 'buf_set_var', 'buf_del_var', 'buf_set_keymap', 'buf_del_keymap', 'win_get_buf', 'win_set_buf', 'win_get_position', 'win_set_height', 'win_get_height', 'win_set_width', 'win_get_width', 'win_set_cursor', 'win_get_cursor', 'win_set_option', 'win_get_option', 'win_get_var', 'win_set_var', 'win_del_var', 'win_is_valid', 'win_get_number', 'win_get_tabpage', 'win_close', 'tabpage_get_number', 'tabpage_list_wins', 'tabpage_get_var', 'tabpage_set_var', 'tabpage_del_var', 'tabpage_is_valid', 'tabpage_get_win', ] # helper {{ # Create a window with bufnr for execute win_execute def CreatePopup(bufnr: number): number noa const id = popup_create(bufnr, { \ 'line': 1, \ 'col': &columns, \ 'maxwidth': 1, \ 'maxheight': 1, \ }) popup_hide(id) return id enddef def CheckBufnr(bufnr: number): void if bufnr != 0 && !bufexists(bufnr) throw $'Invalid buffer id: {bufnr}' endif enddef def CheckWinid(winid: number): void if winid != 0 && empty(getwininfo(winid)) throw $'Invalid window id: {winid}' endif enddef def GetValidBufnr(id: number): number if id == 0 return bufnr('%') endif if !bufexists(id) throw $'Invalid buffer id: {id}' endif return id enddef def GetValidWinid(id: number): number if id == 0 return win_getid() endif if empty(getwininfo(id)) throw $'Invalid window id: {id}' endif return id enddef def CheckKey(dict: dict, key: string): void if !has_key(dict, key) throw $'Key not found: {key}' endif enddef # TextChanged and callback not fired when using channel on vim. export def OnTextChange(bufnr: number): void const event = mode() ==# 'i' ? 'TextChangedI' : 'TextChanged' if bufnr('%') == bufnr coc#compat#execute($'doautocmd {event}') else BufExecute(bufnr, [$'legacy doautocmd {event}']) endif enddef def Pick(target: dict, source: dict, keys: list): void for key in keys if has_key(source, key) target[key] = source[key] endif endfor enddef # execute command for bufnr export def BufExecute(bufnr: number, cmds: list, silent = 'silent'): void var winid = get(win_findbuf(bufnr), 0, -1) var need_close: bool = false if winid == -1 winid = CreatePopup(bufnr) need_close = true endif win_execute(winid, cmds, silent) if need_close noa popup_close(winid) endif enddef def BufLineCount(bufnr: number): number const info = get(getbufinfo(bufnr), 0, null) if empty(info) throw $'Invalid buffer id: {bufnr}' endif return info.loaded == 0 ? 0 : info.linecount enddef def IsPopup(winid: number): bool return index(popup_list(), winid) != -1 enddef def TabIdNr(tid: number): number if tid == 0 return tabpagenr() endif var result: any = null for nr in range(1, tabpagenr('$')) if gettabvar(nr, '__coc_tid', null) == tid result = nr endif endfor if result == null throw $'Invalid tabpage id: {tid}' endif return result enddef export def TabNrId(nr: number): number var tid = gettabvar(nr, '__coc_tid', -1) if tid == -1 tid = tab_id settabvar(nr, '__coc_tid', tid) tab_id += 1 endif return tid enddef def WinTabnr(winid: number): number const info = getwininfo(winid) if empty(info) throw $'Invalid window id: {winid}' endif return info[0]['tabnr'] enddef def DeferExecute(cmd: string): void def RunExecute(): void if cmd =~# '^redraw' if index(['c', 'r'], mode()) == -1 execute cmd endif elseif cmd =~# '^echo' execute cmd else silent! execute $'legacy {cmd}' endif enddef timer_start(0, (..._) => RunExecute()) enddef def InspectType(val: any): string return get(known_types, type(val), 'Unknown') enddef def EscapeSpace(text: string): string return substitute(text, ' ', '', 'g') enddef # See :h option-backslash def EscapeOptionValue(value: any): string if type(value) == v:t_string return substitute(value, '\( \|\\\)', '\\\1', 'g') endif return string(value) enddef # Check the type like nvim, currently bool option only def CheckOptionValue(name: string, value: any): void if index(boolean_options, name) != -1 && type(value) != v:t_bool throw $"Invalid value for option '{name}': expected boolean, got {tolower(InspectType(value))} {value}" endif enddef def CheckScopeOption(opts: dict): void if has_key(opts, 'scope') && has_key(opts, 'buf') throw "Can't use both scope and buf" endif if has_key(opts, 'buf') && has_key(opts, 'win') throw "Can't use both buf and win" endif if has_key(opts, 'scope') && index(scopes, opts.scope) == -1 throw "Invalid 'scope': expected 'local' or 'global'" endif if has_key(opts, 'buf') && type(opts.buf) != v:t_number throw $"Invalid 'buf': expected Number, got {InspectType(opts.buf)}" endif if has_key(opts, 'win') && type(opts.win) != v:t_number throw $"Invalid 'win': expected Number, got {InspectType(opts.win)}" endif enddef def CreateModePrefix(mode: string, opts: dict): string if mode ==# '!' return 'map!' endif return get(opts, 'noremap', 0) ? $'{mode}noremap' : $'{mode}map' enddef def CreateArguments(opts: dict): string var arguments = '' for key in keys(opts) if opts[key] == true && index(keymap_arguments, key) != -1 arguments ..= $'<{key}>' endif endfor return arguments enddef export def GeneratePropId(bufnr: number): number const max: number = get(buffer_id, bufnr, prop_offset) const id: number = max + 1 buffer_id[bufnr] = id return id enddef export def GetNamespaceTypes(ns: number): list if ns == -1 return values(id_types)->flattennew(1) endif return get(id_types, ns, []) enddef export def CreateType(ns: number, hl: string, opts: dict): string const type: string = $'{hl}_{ns}' final types: list = get(id_types, ns, []) if index(types, type) == -1 add(types, type) id_types[ns] = types if empty(prop_type_get(type)) final type_option: dict = {'highlight': hl} if !hlexists(hl) execute $'highlight default {hl} ctermfg=NONE' endif const hl_mode: string = get(opts, 'hl_mode', 'combine') if hl_mode !=# 'combine' type_option['override'] = 1 type_option['combine'] = 0 endif # vim not throw for unknown properties prop_type_add(type, extend(type_option, opts)) endif endif return type enddef def OnBufferChange(bufnr: number, start: number, end: number, added: number, bufchanges: list): void const new_len = end - start + added const lines: list = new_len > 0 ? getbufline(bufnr, start, start + new_len - 1) : [] coc#rpc#notify('vim_buf_change_event', [bufnr, getbufvar(bufnr, 'changedtick'), start - 1, end - 1, lines]) enddef export def DetachListener(bufnr: number): bool const id: number = get(listener_map, bufnr, 0) if id != 0 remove(listener_map, bufnr) return listener_remove(id) != 0 endif return false enddef # echo single line message with highlight export def EchoHl(message: string, hl: string): void const escaped = substitute(message, "'", "''", 'g') DeferExecute($"echohl {hl} | echo '{escaped}' | echohl None") enddef def ChangeBufferLines(bufnr: number, start_row: number, start_col: number, end_row: number, end_col: number, replacement: list): void const lines = getbufline(bufnr, start_row + 1, end_row + 1) const total = len(lines) final new_lines = [] const before = strpart(lines[0], 0, start_col) const after = strpart(lines[total - 1], end_col) const last = len(replacement) - 1 for idx in range(0, last) var line = replacement[idx] if idx == 0 line = before .. line endif if idx == last line = line .. after endif new_lines->add(line) endfor const del = end_row - (start_row - 1) if del == last + 1 setbufline(bufnr, start_row + 1, new_lines) else if len(new_lines) > 0 appendbufline(bufnr, start_row, new_lines) endif if del > 0 const lnum = start_row + len(new_lines) + 1 deletebufline(bufnr, lnum, lnum + del - 1) endif endif enddef # make sure inserted space last. def SortProp(a: dict, b: dict): number if a.col != b.col return a.col > b.col ? 1 : -1 endif if has_key(a, 'text') && has_key(b, 'text') return a.text ==# ' ' ? 1 : -1 endif return 0 enddef def ReplaceBufLines(bufnr: number, start_row: number, end_row: number, replacement: list): void if start_row != end_row deletebufline(bufnr, start_row + 1, end_row) endif if !empty(replacement) const new_lines = replacement[0 : -2] if !empty(new_lines) appendbufline(bufnr, start_row, new_lines) endif endif enddef # Change buffer texts with text properties keeped. export def SetBufferText(bufnr: number, start_row: number, start_col: number, end_row: number, end_col: number, replacement: list): void # Improve speed for replace lines if start_col == 0 && end_col == 0 && (empty(replacement) || replacement[len(replacement) - 1] == '') ReplaceBufLines(bufnr, start_row, end_row, replacement) else const lines = getbufline(bufnr, start_row + 1, end_row + 1) final new_props = [] const props = prop_list(start_row + 1, { 'bufnr': bufnr, 'end_lnum': end_row + 1 }) const total = len(props) const replace = empty(replacement) ? [''] : replacement if total > 0 var idx = 0 while idx != total const prop = props[idx] if !prop.start || !prop.end || has_key(prop, 'text_align') idx += 1 continue endif if prop.lnum > start_row + 1 || prop.col + get(prop, 'length', 0) > start_col + 1 break endif new_props->add(prop) idx += 1 endwhile const rl = len(replace) if idx != total # new - old const line_delta = start_row + rl - 1 - end_row var col_delta = 0 if rl > 1 col_delta = strlen(replace[rl - 1]) - end_col else col_delta = start_col + strlen(replace[0]) - end_col endif while idx != total var prop = props[idx] if prop.lnum < end_row + 1 || prop.col < end_col + 1 || !prop.start || !prop.end || has_key(prop, 'text_align') idx += 1 continue endif if prop.lnum > end_row + 1 break endif prop = copy(prop) prop.lnum += line_delta prop.col += col_delta new_props->add(prop) idx += 1 endwhile endif endif ChangeBufferLines(bufnr, start_row, start_col, end_row, end_col, replace) for prop in sort(new_props, SortProp) const has_text = has_key(prop, 'text') const id = get(prop, 'id', -1) if id < 0 && !has_text # prop.id < 0 should be vtext, but text not exists on old vim, can't handle continue endif final opts = {'bufnr': bufnr, 'type': prop.type} if id > 0 opts.id = prop.id opts.length = get(prop, 'length', 0) else Pick(opts, prop, ['text', 'text_wrap']) endif prop_add(prop.lnum, prop.col, opts) endfor endif enddef # Change lines only export def SetBufferLines(bufnr: number, start_line: number, end_line: number, replacement: list): void const delCount = end_line - (start_line - 1) const total = len(replacement) if delCount == total const currentLines = getbufline(bufnr, start_line, start_line + delCount) for idx in range(0, delCount - 1) if currentLines[idx] !=# replacement[idx] setbufline(bufnr, start_line + idx, replacement[idx]) endif endfor else if total > 0 appendbufline(bufnr, start_line - 1, replacement) endif if delCount > 0 const start = start_line + total silent deletebufline(bufnr, start, start + delCount - 1) endif endif enddef # }}" # nvim client methods {{ export def Set_current_dir(dir: string): any execute $'legacy cd {fnameescape(dir)}' return null enddef export def Set_var(name: string, value: any): any g:[name] = value return null enddef export def Del_var(name: string): any CheckKey(g:, name) remove(g:, name) return null enddef export def Set_option(name: string, value: any, local: bool = false): any CheckOptionValue(name, value) if index(boolean_options, name) != -1 if value execute $'legacy set{local ? 'l' : ''} {name}' else execute $'legacy set{local ? 'l' : ''} no{name}' endif else execute $"legacy set{local ? 'l' : ''} {name}={EscapeOptionValue(value)}" endif return null enddef export def Get_option(name: string): any return eval($'&{name}') enddef export def Set_current_buf(bufnr: number): any CheckBufnr(bufnr) # autocmd could fail when not use legacy. execute $'legacy buffer {bufnr}' return null enddef export def Set_current_win(winid: number): any CheckWinid(winid) win_gotoid(winid) return null enddef export def Set_current_tabpage(tid: number): any const nr = TabIdNr(tid) execute $'legacy normal! {nr}gt' return null enddef export def List_wins(): list return getwininfo()->map((_, info) => info.winid) enddef export def Call_atomic(calls: list): list final results: list = [] for i in range(len(calls)) const key: string = calls[i][0] const name: string = $"{toupper(key[5])}{strpart(key, 6)}" try const result = call(name, get(calls[i], 1, [])) add(results, result) catch /.*/ return [results, [i, $'VimException({InspectType(v:exception)})', $'{v:exception} on function coc#api#{name}']] endtry endfor return [results, null] enddef export def Set_client_info(..._): any # not supported return null enddef export def Subscribe(..._): any # not supported return null enddef export def Unsubscribe(..._): any # not supported return null enddef # Not return on notification for possible void function call. export def Call_function(method: string, args: list, notify: bool = false): any if method ==# 'execute' return call('coc#compat#execute', args) elseif method ==# 'eval' return Eval(args[0]) elseif method ==# 'win_execute' return call('coc#compat#win_execute', args) elseif !notify return call(method, args) endif call call(method, args) return null enddef export def Call_dict_function(dict: any, method: string, args: list): any if type(dict) == v:t_string return call(method, args, Eval(dict)) endif return call(method, args, dict) enddef # Use the legacy eval, could be called by Call export function Eval(expr) abort legacy return coc#compat#eval(a:expr) endfunction export def Command(command: string): any # command that could cause cursor vanish if command =~# '^\(echo\|redraw\|sign\)' DeferExecute(command) else # Use legacy command not work for command like autocmd coc#compat#execute(command) # The error is set by python script, since vim not give error on python command failure if strpart(command, 0, 2) ==# 'py' const errmsg: string = get(g:, 'errmsg', '') if !empty(errmsg) remove(g:, 'errmsg') throw $'Python error {errmsg}' endif endif endif return null enddef export def Get_api_info(): any const functions: list = map(copy(API_FUNCTIONS), (_, val) => $'nvim_{val}') const channel: any = coc#rpc#get_channel() if empty(channel) throw 'Unable to get channel' endif return [ch_info(channel)['id'], {'functions': functions}] enddef export def List_bufs(): list return getbufinfo()->map((_, info) => info.bufnr) enddef export def Feedkeys(keys: string, mode: string, escape_csi: any = false): any feedkeys(keys, mode) return null enddef export def List_runtime_paths(): list return map(globpath(&runtimepath, '', 0, 1), (_, val) => coc#util#win32unix_to_node(val)) enddef export def Command_output(cmd: string): string const output = coc#compat#execute(cmd, 'silent') # The same as nvim. if cmd =~# '^echo' return trim(output, "\r\n") endif return output enddef export def Exec(code: string, output: bool): string if output return Command_output(code) endif coc#compat#execute(code) return '' enddef # Queues raw user-input, <" is special. To input a literal "<", send . export def Input(keys: string): any const escaped: string = substitute(keys, '<', '\\<', 'g') feedkeys(eval($'"{escaped}"'), 'n') return null enddef export def Create_buf(listed: bool, scratch: bool): number const bufnr: number = bufadd('') setbufvar(bufnr, '&buflisted', listed ? 1 : 0) if scratch setbufvar(bufnr, '&modeline', 0) setbufvar(bufnr, '&buftype', 'nofile') setbufvar(bufnr, '&swapfile', 0) endif bufload(bufnr) return bufnr enddef export def Get_current_line(): string return getline('.') enddef export def Set_current_line(line: string): any setline('.', line) OnTextChange(bufnr('%')) return null enddef export def Del_current_line(): any deletebufline('%', line('.')) OnTextChange(bufnr('%')) return null enddef export def Get_var(var: string): any CheckKey(g:, var) return g:[var] enddef export def Get_vvar(var: string): any return eval($'v:{var}') enddef export def Get_current_buf(): number return bufnr('%') enddef export def Get_current_win(): number return win_getid() enddef export def Get_current_tabpage(): number return TabNrId(tabpagenr()) enddef export def List_tabpages(): list final ids = [] for nr in range(1, tabpagenr('$')) add(ids, TabNrId(nr)) endfor return ids enddef export def Get_mode(): dict const m: string = mode() return {'blocking': m =~# '^r' ? true : false, 'mode': m} enddef export def Strwidth(str: string): number return strwidth(str) enddef export def Out_write(str: string): any echon str DeferExecute('redraw') return null enddef export def Err_write(str: string): any # Err_write texts are cached by node-client return null enddef export def Err_writeln(str: string): any echohl ErrorMsg echom str echohl None DeferExecute('redraw') return null enddef export def Create_namespace(name: string): number if empty(name) const id = namespace_id namespace_id += 1 return id endif var id = get(namespace_cache, name, 0) if id == 0 id = namespace_id namespace_id += 1 namespace_cache[name] = id endif return id enddef export def Get_namespaces(): dict return copy(namespace_cache) enddef export def Set_keymap(mode: string, lhs: string, rhs: string, opts: dict): any const modekey: string = CreateModePrefix(mode, opts) const arguments: string = CreateArguments(opts) const escaped: string = empty(rhs) ? '' : EscapeSpace(rhs) coc#compat#execute($'{modekey} {arguments} {EscapeSpace(lhs)} {escaped}') return null enddef export def Del_keymap(mode: string, lhs: string): any const escaped = substitute(lhs, ' ', '', 'g') execute $'legacy silent {mode}unmap {escaped}' return null enddef export def Set_option_value(name: string, value: any, opts: dict): any CheckScopeOption(opts) const winid: number = get(opts, 'win', -1) const bufnr: number = get(opts, 'buf', -1) const scope: string = get(opts, 'scope', 'global') if bufnr != -1 Buf_set_option(bufnr, name, value) elseif winid != -1 Win_set_option(winid, name, value) else if scope ==# 'global' Set_option(name, value) else Set_option(name, value, true) endif endif return null enddef export def Get_option_value(name: string, opts: dict = {}): any CheckScopeOption(opts) const winid: number = get(opts, 'win', -1) const bufnr: number = get(opts, 'buf', -1) const scope: string = get(opts, 'scope', 'global') var result: any = null if bufnr != -1 result = Buf_get_option(bufnr, name) elseif winid != -1 result = Win_get_option(winid, name) else if scope ==# 'global' result = eval($'&{name}') else result = gettabwinvar(tabpagenr(), 0, '&' .. name, null) if result == null result = Buf_get_option(bufnr('%'), name) endif endif endif return result enddef export def Create_augroup(name: string, option: dict = {}): number const clear: bool = get(option, 'clear', true) if clear execute $'augroup {name} | autocmd! | augroup END' else execute $'augroup {name} | augroup END' endif const id = group_id groups_map[id] = name group_id += 1 return id enddef export def Create_autocmd(event: any, option: dict = {}): number final opt: dict = { event: event } if has_key(option, 'group') if type(option.group) == v:t_number if !has_key(groups_map, option.group) throw $'Invalid group {option.group}' endif opt.group = groups_map[option.group] elseif type(option.group) == v:t_string opt.group = option.group else throw $'Invalid group {option.group}' endif endif if get(option, 'nested', false) == true opt.nested = true endif if get(option, 'once', false) == true opt.once = true endif if has_key(option, 'pattern') opt.pattern = option.pattern else # nvim add it automatically opt.pattern = '*' endif if has_key(option, 'buffer') opt.bufnr = option.buffer endif if has_key(option, 'command') opt.cmd = $'legacy {option.command}' endif call autocmd_add([extend({'replace': get(option, 'replace', false)}, opt)]) const id = autocmd_id autocmds_map[id] = opt autocmd_id += 1 return id enddef export def Del_autocmd(id: number): bool if !has_key(autocmds_map, id) return true endif final opt: dict = autocmds_map[id] # vim add autocmd when cmd exists remove(opt, 'cmd') remove(autocmds_map, id) return autocmd_delete([opt]) enddef # }} # buffer methods {{ export def Buf_set_option(id: number, name: string, value: any): any const bufnr = GetValidBufnr(id) CheckOptionValue(name, value) if index(buffer_options, name) == -1 throw $"Invalid buffer option name: {name}" endif setbufvar(bufnr, $'&{name}', value) return null enddef export def Buf_get_option(id: number, name: string): any const bufnr = GetValidBufnr(id) if index(buffer_options, name) == -1 throw $"Invalid buffer option name: {name}" endif return getbufvar(bufnr, $'&{name}') enddef export def Buf_get_changedtick(id: number): number const bufnr = GetValidBufnr(id) return getbufvar(bufnr, 'changedtick') enddef export def Buf_is_valid(bufnr: number): bool return bufexists(bufnr) enddef export def Buf_is_loaded(bufnr: number): bool return bufloaded(bufnr) enddef export def Buf_get_mark(id: number, name: string): list const bufnr = GetValidBufnr(id) const marks: list = getmarklist(bufnr) for item in marks if item['mark'] ==# $"'{name}" const pos: list = item['pos'] return [pos[1], pos[2] - 1] endif endfor return [0, 0] enddef export def Buf_add_highlight(id: number, srcId: number, hlGroup: string, line: number, colStart: number, colEnd: number, propTypeOpts: dict = {}): any const bufnr = GetValidBufnr(id) var sourceId: number if srcId == 0 max_src_id += 1 sourceId = max_src_id else sourceId = srcId endif Buf_add_highlight1(bufnr, sourceId, hlGroup, line, colStart, colEnd, propTypeOpts) return sourceId enddef # To be called directly for better performance # 0 based line, colStart, colEnd, see `:h prop_type_add` for propTypeOpts export def Buf_add_highlight1(bufnr: number, srcId: number, hlGroup: string, line: number, colStart: number, colEnd: number, propTypeOpts: dict = {}): void const columnEnd: number = colEnd == -1 ? strlen(get(getbufline(bufnr, line + 1), 0, '')) + 1 : colEnd + 1 if columnEnd <= colStart return endif const propType: string = CreateType(srcId, hlGroup, propTypeOpts) const propId: number = GeneratePropId(bufnr) try prop_add(line + 1, colStart + 1, {'bufnr': bufnr, 'type': propType, 'id': propId, 'end_col': columnEnd}) catch /^Vim\%((\a\+)\)\=:\(E967\|E964\)/ # ignore 967 endtry enddef export def Buf_clear_namespace(id: number, srcId: number, startLine: number, endLine: number): any const bufnr = GetValidBufnr(id) const start = startLine + 1 const end = endLine == -1 ? BufLineCount(bufnr) : endLine if srcId == -1 if has_key(buffer_id, bufnr) remove(buffer_id, bufnr) endif prop_clear(start, end, {'bufnr': bufnr}) else const types = get(id_types, srcId, []) if !empty(types) try prop_remove({'bufnr': bufnr, 'all': true, 'types': types}, start, end) catch /^Vim\%((\a\+)\)\=:E968/ # ignore 968 endtry endif endif return null enddef export def Buf_line_count(bufnr: number): number if bufnr == 0 return line('$') endif return BufLineCount(bufnr) enddef export def Buf_attach(id: number = 0, ..._): bool const bufnr = GetValidBufnr(id) # listener not removed on e! DetachListener(bufnr) const result = listener_add(OnBufferChange, bufnr) if result != 0 listener_map[bufnr] = result return true endif return false enddef export def Buf_detach(id: number): bool const bufnr = GetValidBufnr(id) return DetachListener(bufnr) enddef export def Buf_flush(id: any): void if type(id) == v:t_number && has_key(listener_map, id) listener_flush(id) endif enddef export def Buf_get_lines(id: number, start: number, end: number, strict: bool = false): list const bufnr = GetValidBufnr(id) const len = BufLineCount(bufnr) const s = start < 0 ? len + start + 2 : start + 1 const e = end < 0 ? len + end + 1 : end if strict && e > len throw $'Index out of bounds {end}' endif return getbufline(bufnr, s, e) enddef export def Buf_set_lines(id: number, start: number, end: number, strict: bool = false, replacement: list = []): any const bufnr = GetValidBufnr(id) const len = BufLineCount(bufnr) const startLnum = start < 0 ? len + start + 2 : start + 1 var endLnum = end < 0 ? len + end + 1 : end if endLnum > len if strict throw $'Index out of bounds {end}' else endLnum = len endif endif const view = bufnr == bufnr('%') ? winsaveview() : null SetBufferLines(bufnr, startLnum, endLnum, replacement) if view != null winrestview(view) endif OnTextChange(bufnr) return null enddef export def Buf_set_name(id: number, name: string): any const bufnr = GetValidBufnr(id) BufExecute(bufnr, ['legacy silent noa 0file', $'legacy file {fnameescape(name)}']) return null enddef export def Buf_get_name(id: number): string return GetValidBufnr(id)->bufname() enddef export def Buf_get_var(id: number, name: string): any const bufnr = GetValidBufnr(id) const dict: dict = getbufvar(bufnr, '') CheckKey(dict, name) return dict[name] enddef export def Buf_set_var(id: number, name: string, val: any): any const bufnr = GetValidBufnr(id) setbufvar(bufnr, name, val) return null enddef export def Buf_del_var(id: number, name: string): any const bufnr = GetValidBufnr(id) final bufvars = getbufvar(bufnr, '') CheckKey(bufvars, name) remove(bufvars, name) return null enddef export def Buf_set_keymap(id: number, mode: string, lhs: string, rhs: string, opts: dict): any const bufnr = GetValidBufnr(id) const prefix = CreateModePrefix(mode, opts) const arguments = CreateArguments(opts) const escaped = empty(rhs) ? '' : EscapeSpace(rhs) BufExecute(bufnr, [$'legacy {prefix} {arguments} {EscapeSpace(lhs)} {escaped}']) return null enddef export def Buf_del_keymap(id: number, mode: string, lhs: string): any const bufnr = GetValidBufnr(id) const escaped = substitute(lhs, ' ', '', 'g') BufExecute(bufnr, [$'legacy silent {mode}unmap {escaped}']) return null enddef export def Buf_set_text(id: number, start_row: number, start_col: number, end_row: number, end_col: number, replacement: list): void const bufnr = GetValidBufnr(id) const len = BufLineCount(bufnr) if start_row >= len throw $'Start row out of bounds {start_row}' endif if end_row >= len throw $'End row out of bounds {end_row}' endif SetBufferText(bufnr, start_row, start_col, end_row, end_col, replacement) enddef # }} # window methods {{ export def Win_get_buf(id: number): number return GetValidWinid(id)->winbufnr() enddef export def Win_set_buf(id: number, bufnr: number): any const winid = GetValidWinid(id) CheckBufnr(bufnr) win_execute(winid, $'legacy buffer {bufnr}') return null enddef export def Win_get_position(id: number): list const winid = GetValidWinid(id) const [row, col] = win_screenpos(winid) if row == 0 && col == 0 throw $'Invalid window {winid}' endif return [row - 1, col - 1] enddef export def Win_set_height(id: number, height: number): any const winid = GetValidWinid(id) if IsPopup(winid) popup_move(winid, {'maxheight': height, 'minheight': height}) else win_execute(winid, $'legacy resize {height}') endif return null enddef export def Win_get_height(id: number): number const winid = GetValidWinid(id) if IsPopup(winid) return popup_getpos(winid)['height'] endif return winheight(winid) enddef export def Win_set_width(id: number, width: number): any const winid = GetValidWinid(id) if IsPopup(winid) popup_move(winid, {'maxwidth': width, 'minwidth': width}) else win_execute(winid, $'legacy vertical resize {width}') endif return null enddef export def Win_get_width(id: number): number const winid = GetValidWinid(id) if IsPopup(winid) return popup_getpos(winid)['width'] endif return winwidth(winid) enddef export def Win_set_cursor(id: number, pos: list): any const winid = GetValidWinid(id) win_execute(winid, $'cursor({pos[0]}, {pos[1] + 1})') return null enddef export def Win_get_cursor(id: number): list const winid = GetValidWinid(id) const result = getcurpos(winid) if result[1] == 0 return [1, 0] endif return [result[1], result[2] - 1] enddef export def Win_set_option(id: number, name: string, value: any): any const winid = GetValidWinid(id) CheckOptionValue(name, value) const tabnr = WinTabnr(winid) if index(window_options, name) == -1 throw $"Invalid window option name: {name}" endif settabwinvar(tabnr, winid, $'&{name}', value) return null enddef export def Win_get_option(id: number, name: string, ..._): any const winid = GetValidWinid(id) const tabnr = WinTabnr(winid) if index(window_options, name) == -1 throw $"Invalid window option name: {name}" endif return gettabwinvar(tabnr, winid, '&' .. name) enddef export def Win_get_var(id: number, name: string, ..._): any const winid = GetValidWinid(id) const tabnr = WinTabnr(winid) const vars = gettabwinvar(tabnr, winid, '') CheckKey(vars, name) return get(vars, name, null) enddef export def Win_set_var(id: number, name: string, value: any): any const winid = GetValidWinid(id) const tabnr = WinTabnr(winid) settabwinvar(tabnr, winid, name, value) return null enddef export def Win_del_var(id: number, name: string): any const winid = GetValidWinid(id) const tabnr = WinTabnr(winid) const vars: dict = gettabwinvar(tabnr, winid, '') CheckKey(vars, name) win_execute(winid, 'remove(w:, "' .. name .. '")') return null enddef export def Win_is_valid(id: number): bool const winid = id == 0 ? win_getid() : id return empty(getwininfo(winid)) == 0 enddef export def Win_get_number(id: number): number const winid = GetValidWinid(id) const info = getwininfo(winid) # Note: vim return 0 for popup return info[0]['winnr'] enddef # Not work for popup since vim gives 0 for tabnr export def Win_get_tabpage(id: number): number return GetValidWinid(id)->WinTabnr()->TabNrId() enddef export def Win_close(id: number, force: bool = false): any const winid = GetValidWinid(id) if IsPopup(winid) popup_close(winid) else win_execute(winid, $'legacy close{force ? '!' : ''}') endif return null enddef # }} # tabpage methods {{ export def Tabpage_get_number(tid: number): number return TabIdNr(tid) enddef export def Tabpage_list_wins(tid: number): list return TabIdNr(tid)->gettabinfo()[0].windows enddef export def Tabpage_get_var(tid: number, name: string): any const nr = TabIdNr(tid) const dict = gettabvar(nr, '') CheckKey(dict, name) return get(dict, name, null) enddef export def Tabpage_set_var(tid: number, name: string, value: any): any const nr = TabIdNr(tid) settabvar(nr, name, value) return null enddef export def Tabpage_del_var(tid: number, name: string): any const nr = TabIdNr(tid) final dict = gettabvar(nr, '') CheckKey(dict, name) remove(dict, name) return null enddef export def Tabpage_is_valid(tid: number): bool for nr in range(1, tabpagenr('$')) if gettabvar(nr, '__coc_tid', -1) == tid return true endif endfor return false enddef export def Tabpage_get_win(tid: number): number const nr = TabIdNr(tid) return win_getid(tabpagewinnr(nr), nr) enddef export def Tabpage_ids(): void for nr in range(1, tabpagenr('$')) if gettabvar(nr, '__coc_tid', -1) == -1 settabvar(nr, '__coc_tid', tab_id) tab_id += 1 endif endfor enddef # }} # Used by node-client request, function needed to catch error # Must use coc#api# prefix to avoid call global function export function Call(method, args) abort let err = v:null let result = v:null try let result = call($'coc#api#{toupper(a:method[0])}{strpart(a:method, 1)}', a:args) call coc#api#Buf_flush(bufnr('%')) catch /.*/ let err = v:exception .. " - on request \"" .. a:method .. "\" \n" .. v:throwpoint let result = v:null endtry return [err, result] endfunction # Used by node-client notification, function needed to catch error export function Notify(method, args) abort try if a:method ==# 'call_function' call coc#api#Call_function(a:args[0], a:args[1], v:true) else let fname = $'coc#api#{toupper(a:method[0])}{strpart(a:method, 1)}' call call(fname, a:args) endif call coc#api#Buf_flush(bufnr('%')) catch /.*/ call coc#rpc#notify('nvim_error_event', [0, v:exception .. " - on notification \"" .. a:method .. "\" \n" .. v:throwpoint]) endtry return v:null endfunction # Could be called by other plugin const call_function =<< trim END function! coc#api#call(method, args) abort return coc#api#Call(a:method, a:args) endfunction END execute $'legacy execute "{join(call_function, '\n')}"' defcompile # vim: set sw=2 ts=2 sts=2 et tw=78 foldmarker={{,}} foldmethod=marker foldlevel=0: ================================================ FILE: autoload/coc/client.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:clients = {} if get(g:, 'node_client_debug', 0) echohl WarningMsg | echo '[coc.nvim] Enable g:node_client_debug could impact your vim experience' | echohl None let $NODE_CLIENT_LOG_LEVEL = 'debug' if exists('$NODE_CLIENT_LOG_FILE') let s:logfile = resolve($NODE_CLIENT_LOG_FILE) else let s:logfile = tempname() let $NODE_CLIENT_LOG_FILE = s:logfile endif endif " create a client function! coc#client#create(name, command) let client = {} let client['command'] = a:command let client['name'] = a:name let client['running'] = 0 let client['async_req_id'] = 1 let client['async_callbacks'] = {} " vim only let client['channel'] = v:null " neovim only let client['chan_id'] = 0 let client['start'] = function('s:start', [], client) let client['request'] = function('s:request', [], client) let client['notify'] = function('s:notify', [], client) let client['request_async'] = function('s:request_async', [], client) let client['on_async_response'] = function('s:on_async_response', [], client) let s:clients[a:name] = client return client endfunction function! s:start() dict if self.running | return | endif if !isdirectory(getcwd()) echoerr '[coc.nvim] Current cwd is not a valid directory.' return endif let tmpdir = fnamemodify(tempname(), ':p:h') let env = { 'NODE_NO_WARNINGS': '1', 'TMPDIR': coc#util#win32unix_to_node(tmpdir)} if s:is_vim let env['VIM_NODE_RPC'] = 1 if get(g:, 'node_client_debug', 0) || $COC_VIM_CHANNEL_ENABLE == '1' let file = tmpdir . '/coc.log' call ch_logfile(file, 'w') echohl MoreMsg | echo '[coc.nvim] channel log to '.file | echohl None endif let options = { \ 'noblock': 1, \ 'in_mode': 'json', \ 'out_mode': 'json', \ 'err_mode': 'nl', \ 'err_cb': {channel, message -> s:on_stderr(self.name, split(message, "\n"))}, \ 'exit_cb': {channel, code -> s:on_exit(self.name, code)}, \ 'env': env \} let job = job_start(self.command, options) let status = job_status(job) if status !=# 'run' let self.running = 0 echohl Error | echom 'Failed to start '.self.name.' service' | echohl None return endif let self['channel'] = job_getchannel(job) else let opts = { \ 'rpc': 1, \ 'on_stderr': {channel, msgs -> s:on_stderr(self.name, msgs)}, \ 'on_exit': {channel, code -> s:on_exit(self.name, code)}, \ 'env': env \ } let chan_id = jobstart(self.command, opts) if chan_id <= 0 echohl Error | echom 'Failed to start '.self.name.' service' | echohl None return endif let self['chan_id'] = chan_id endif let self['running'] = 1 endfunction function! s:on_stderr(name, msgs) if get(g:, 'coc_vim_leaving', 0) | return | endif let data = filter(copy(a:msgs), '!empty(v:val)') if empty(data) | return | endif let client = a:name ==# 'coc' ? '[coc.nvim]' : '['.a:name.']' let data[0] = client.': '.data[0] if a:name ==# 'coc' && len(filter(copy(data), 'v:val =~# "SyntaxError: "')) call coc#client#check_version() return endif if get(g:, 'coc_disable_uncaught_error', 0) | return | endif call s:on_error(a:name, data) endfunction function! coc#client#check_version() abort if (has_key(g:, 'coc_node_path')) let node = expand(g:coc_node_path) else let node = $COC_NODE_PATH == '' ? 'node' : $COC_NODE_PATH endif let cmd = node . ' --version' let output = system(cmd) let msgs = [] if v:shell_error let msgs = ['Unexpected result from "'.cmd.'"'] + split(output, '\n') else let ms = matchlist(output, 'v\(\d\+\).\(\d\+\).\(\d\+\)') if empty(ms) let msgs = ['Unable to get node version by "'.cmd.'" please install NodeJS from https://nodejs.org/en/download/'] elseif str2nr(ms[1]) < 16 || (str2nr(ms[1]) == 16 && str2nr(ms[2]) < 18) let msgs = ['Current Node.js version '.trim(output).' < 16.18.0 ', 'Please upgrade your Node.js'] endif endif if !empty(msgs) call s:on_error('coc', msgs) endif endfunction function! s:on_exit(name, code) abort if get(g:, 'coc_vim_leaving', 0) | return | endif let client = get(s:clients, a:name, v:null) if empty(client) | return | endif if client['running'] != 1 | return | endif let client['running'] = 0 let client['chan_id'] = 0 let client['channel'] = v:null let client['async_req_id'] = 1 if a:code != 0 && a:code != 143 && a:code != -1 echohl Error | echom 'client '.a:name. ' abnormal exit with: '.a:code | echohl None endif endfunction function! coc#client#get_client(name) abort return get(s:clients, a:name, v:null) endfunction function! coc#client#get_channel(client) if s:is_vim return a:client['channel'] endif return a:client['chan_id'] endfunction function! s:request(method, args) dict let channel = coc#client#get_channel(self) if empty(channel) | return '' | endif try if s:is_vim let res = ch_evalexpr(channel, [a:method, a:args], {'timeout': 60 * 1000}) if type(res) == 1 && res ==# '' throw 'request '.a:method. ' '.string(a:args).' timeout after 60s' endif let [l:errmsg, res] = res if !empty(l:errmsg) throw 'Error on "'.a:method.'" request: '.l:errmsg else return res endif else return call('rpcrequest', [channel, a:method] + a:args) endif catch /.*/ if v:exception =~# 'E475' if get(g:, 'coc_vim_leaving', 0) | return | endif echohl Error | echom '['.self.name.'] server connection lost' | echohl None let name = self.name call s:on_exit(name, 0) execute 'silent do User ConnectionLost'.toupper(name[0]).name[1:] elseif v:exception =~# 'E12' " neovim's bug, ignore it else if s:is_vim throw v:exception else throw 'Error on request: '.v:exception endif endif endtry endfunction function! s:notify(method, args) dict let channel = coc#client#get_channel(self) if empty(channel) return '' endif try if s:is_vim call ch_sendraw(channel, json_encode([0, [a:method, a:args]])."\n") else call call('rpcnotify', [channel, a:method] + a:args) endif catch /.*/ if v:exception =~# 'E475' if get(g:, 'coc_vim_leaving', 0) return endif echohl Error | echom '['.self.name.'] server connection lost' | echohl None let name = self.name call s:on_exit(name, 0) execute 'silent do User ConnectionLost'.toupper(name[0]).name[1:] elseif v:exception =~# 'E12' " neovim's bug, ignore it else echohl Error | echo 'Error on notify ('.a:method.'): '.v:exception | echohl None endif endtry endfunction function! s:request_async(method, args, cb) dict let channel = coc#client#get_channel(self) if empty(channel) | return '' | endif if type(a:cb) != 2 echohl Error | echom '['.self['name'].'] Callback should be function' | echohl None return endif let id = self.async_req_id let self.async_req_id = id + 1 let self.async_callbacks[id] = a:cb call self['notify']('nvim_async_request_event', [id, a:method, a:args]) endfunction function! s:on_async_response(id, resp, isErr) dict let Callback = get(self.async_callbacks, a:id, v:null) if empty(Callback) " should not happen echohl Error | echom 'callback not found' | echohl None return endif call remove(self.async_callbacks, a:id) if a:isErr call call(Callback, [a:resp, v:null]) else call call(Callback, [v:null, a:resp]) endif endfunction function! coc#client#is_running(name) abort let client = get(s:clients, a:name, v:null) if empty(client) | return 0 | endif if !client['running'] | return 0 | endif try if s:is_vim let status = job_status(ch_getjob(client['channel'])) return status ==# 'run' else let chan_id = client['chan_id'] let [code] = jobwait([chan_id], 10) return code == -1 endif catch /.*/ return 0 endtry endfunction function! coc#client#stop(name) abort let client = get(s:clients, a:name, v:null) if empty(client) | return 1 | endif let running = coc#client#is_running(a:name) if !running echohl WarningMsg | echom 'client '.a:name. ' not running.' | echohl None return 1 endif if s:is_vim call job_stop(ch_getjob(client['channel']), 'term') else call jobstop(client['chan_id']) endif sleep 200m if coc#client#is_running(a:name) echohl Error | echom 'client '.a:name. ' stop failed.' | echohl None return 0 endif call s:on_exit(a:name, 0) echohl MoreMsg | echom 'client '.a:name.' stopped!' | echohl None return 1 endfunction function! coc#client#kill(name) abort let client = get(s:clients, a:name, v:null) if empty(client) | return 1 | endif let running = coc#client#is_running(a:name) if empty(client) || exists('$COC_NVIM_REMOTE_ADDRESS') return 1 endif if running if s:is_vim call job_stop(ch_getjob(client['channel']), 'kill') else call jobstop(client['chan_id']) endif endif endfunction function! coc#client#request(name, method, args) let client = get(s:clients, a:name, v:null) if !empty(client) return client['request'](a:method, a:args) endif endfunction function! coc#client#notify(name, method, args) let client = get(s:clients, a:name, v:null) if !empty(client) call client['notify'](a:method, a:args) endif endfunction function! coc#client#request_async(name, method, args, cb) let client = get(s:clients, a:name, v:null) if !empty(client) call client['request_async'](a:method, a:args, a:cb) endif endfunction function! coc#client#on_response(name, id, resp, isErr) let client = get(s:clients, a:name, v:null) if !empty(client) call client['on_async_response'](a:id, a:resp, a:isErr) endif endfunction function! coc#client#restart(name) abort let stopped = coc#client#stop(a:name) if !stopped | return | endif let client = get(s:clients, a:name, v:null) if !empty(client) call client['start']() endif endfunction function! coc#client#restart_all() for key in keys(s:clients) call coc#client#restart(key) endfor endfunction function! coc#client#open_log() if !get(g:, 'node_client_debug', 0) throw '[coc.nvim] use let g:node_client_debug = 1 in your vimrc to enable debug mode.' return endif execute 'vs '.s:logfile endfunction function! coc#client#get_log() if !get(g:, 'node_client_debug', 0) throw '[coc.nvim] use let g:node_client_debug = 1 in your vimrc to enable debug mode.' return '' endif return s:logfile endfunction function! s:on_error(name, msgs) abort echohl ErrorMsg echo join(a:msgs, "\n") echohl None let client = get(s:clients, a:name, v:null) if !empty(client) let errors = get(client, 'stderr', []) call extend(errors, a:msgs) let client['stderr'] = errors endif endfunction ================================================ FILE: autoload/coc/color.vim ================================================ scriptencoding utf-8 let s:activate = "" let s:quit = "" if has("gui_macvim") && has('gui_running') let s:app = "MacVim" elseif $TERM_PROGRAM ==# "Apple_Terminal" let s:app = "Terminal" elseif $TERM_PROGRAM ==# "iTerm.app" let s:app = "iTerm2" elseif has('mac') let s:app = "System Events" let s:quit = "quit" let s:activate = 'activate' endif let s:patterns = {} let s:patterns['hex'] = '\v#?(\x{2})(\x{2})(\x{2})' let s:patterns['shortHex'] = '\v#(\x{1})(\x{1})(\x{1})' let s:xterm_colors = { \ '0': '#000000', '1': '#800000', '2': '#008000', '3': '#808000', '4': '#000080', \ '5': '#800080', '6': '#008080', '7': '#c0c0c0', '8': '#808080', '9': '#ff0000', \ '10': '#00ff00', '11': '#ffff00', '12': '#0000ff', '13': '#ff00ff', '14': '#00ffff', \ '15': '#ffffff', '16': '#000000', '17': '#00005f', '18': '#000087', '19': '#0000af', \ '20': '#0000df', '21': '#0000ff', '22': '#005f00', '23': '#005f5f', '24': '#005f87', \ '25': '#005faf', '26': '#005fdf', '27': '#005fff', '28': '#008700', '29': '#00875f', \ '30': '#008787', '31': '#0087af', '32': '#0087df', '33': '#0087ff', '34': '#00af00', \ '35': '#00af5f', '36': '#00af87', '37': '#00afaf', '38': '#00afdf', '39': '#00afff', \ '40': '#00df00', '41': '#00df5f', '42': '#00df87', '43': '#00dfaf', '44': '#00dfdf', \ '45': '#00dfff', '46': '#00ff00', '47': '#00ff5f', '48': '#00ff87', '49': '#00ffaf', \ '50': '#00ffdf', '51': '#00ffff', '52': '#5f0000', '53': '#5f005f', '54': '#5f0087', \ '55': '#5f00af', '56': '#5f00df', '57': '#5f00ff', '58': '#5f5f00', '59': '#5f5f5f', \ '60': '#5f5f87', '61': '#5f5faf', '62': '#5f5fdf', '63': '#5f5fff', '64': '#5f8700', \ '65': '#5f875f', '66': '#5f8787', '67': '#5f87af', '68': '#5f87df', '69': '#5f87ff', \ '70': '#5faf00', '71': '#5faf5f', '72': '#5faf87', '73': '#5fafaf', '74': '#5fafdf', \ '75': '#5fafff', '76': '#5fdf00', '77': '#5fdf5f', '78': '#5fdf87', '79': '#5fdfaf', \ '80': '#5fdfdf', '81': '#5fdfff', '82': '#5fff00', '83': '#5fff5f', '84': '#5fff87', \ '85': '#5fffaf', '86': '#5fffdf', '87': '#5fffff', '88': '#870000', '89': '#87005f', \ '90': '#870087', '91': '#8700af', '92': '#8700df', '93': '#8700ff', '94': '#875f00', \ '95': '#875f5f', '96': '#875f87', '97': '#875faf', '98': '#875fdf', '99': '#875fff', \ '100': '#878700', '101': '#87875f', '102': '#878787', '103': '#8787af', '104': '#8787df', \ '105': '#8787ff', '106': '#87af00', '107': '#87af5f', '108': '#87af87', '109': '#87afaf', \ '110': '#87afdf', '111': '#87afff', '112': '#87df00', '113': '#87df5f', '114': '#87df87', \ '115': '#87dfaf', '116': '#87dfdf', '117': '#87dfff', '118': '#87ff00', '119': '#87ff5f', \ '120': '#87ff87', '121': '#87ffaf', '122': '#87ffdf', '123': '#87ffff', '124': '#af0000', \ '125': '#af005f', '126': '#af0087', '127': '#af00af', '128': '#af00df', '129': '#af00ff', \ '130': '#af5f00', '131': '#af5f5f', '132': '#af5f87', '133': '#af5faf', '134': '#af5fdf', \ '135': '#af5fff', '136': '#af8700', '137': '#af875f', '138': '#af8787', '139': '#af87af', \ '140': '#af87df', '141': '#af87ff', '142': '#afaf00', '143': '#afaf5f', '144': '#afaf87', \ '145': '#afafaf', '146': '#afafdf', '147': '#afafff', '148': '#afdf00', '149': '#afdf5f', \ '150': '#afdf87', '151': '#afdfaf', '152': '#afdfdf', '153': '#afdfff', '154': '#afff00', \ '155': '#afff5f', '156': '#afff87', '157': '#afffaf', '158': '#afffdf', '159': '#afffff', \ '160': '#df0000', '161': '#df005f', '162': '#df0087', '163': '#df00af', '164': '#df00df', \ '165': '#df00ff', '166': '#df5f00', '167': '#df5f5f', '168': '#df5f87', '169': '#df5faf', \ '170': '#df5fdf', '171': '#df5fff', '172': '#df8700', '173': '#df875f', '174': '#df8787', \ '175': '#df87af', '176': '#df87df', '177': '#df87ff', '178': '#dfaf00', '179': '#dfaf5f', \ '180': '#dfaf87', '181': '#dfafaf', '182': '#dfafdf', '183': '#dfafff', '184': '#dfdf00', \ '185': '#dfdf5f', '186': '#dfdf87', '187': '#dfdfaf', '188': '#dfdfdf', '189': '#dfdfff', \ '190': '#dfff00', '191': '#dfff5f', '192': '#dfff87', '193': '#dfffaf', '194': '#dfffdf', \ '195': '#dfffff', '196': '#ff0000', '197': '#ff005f', '198': '#ff0087', '199': '#ff00af', \ '200': '#ff00df', '201': '#ff00ff', '202': '#ff5f00', '203': '#ff5f5f', '204': '#ff5f87', \ '205': '#ff5faf', '206': '#ff5fdf', '207': '#ff5fff', '208': '#ff8700', '209': '#ff875f', \ '210': '#ff8787', '211': '#ff87af', '212': '#ff87df', '213': '#ff87ff', '214': '#ffaf00', \ '215': '#ffaf5f', '216': '#ffaf87', '217': '#ffafaf', '218': '#ffafdf', '219': '#ffafff', \ '220': '#ffdf00', '221': '#ffdf5f', '222': '#ffdf87', '223': '#ffdfaf', '224': '#ffdfdf', \ '225': '#ffdfff', '226': '#ffff00', '227': '#ffff5f', '228': '#ffff87', '229': '#ffffaf', \ '230': '#ffffdf', '231': '#ffffff', '232': '#080808', '233': '#121212', '234': '#1c1c1c', \ '235': '#262626', '236': '#303030', '237': '#3a3a3a', '238': '#444444', '239': '#4e4e4e', \ '240': '#585858', '241': '#606060', '242': '#666666', '243': '#767676', '244': '#808080', \ '245': '#8a8a8a', '246': '#949494', '247': '#9e9e9e', '248': '#a8a8a8', '249': '#b2b2b2', \ '250': '#bcbcbc', '251': '#c6c6c6', '252': '#d0d0d0', '253': '#dadada', '254': '#e4e4e4', \ '255': '#eeeeee'} let s:xterm_16colors = { \ 'black': '#000000', \ 'darkblue': '#00008B', \ 'darkgreen': '#00CD00', \ 'darkcyan': '#00CDCD', \ 'darkred': '#CD0000', \ 'darkmagenta': '#8B008B', \ 'brown': '#CDCD00', \ 'darkyellow': '#CDCD00', \ 'lightgrey': '#E5E5E5', \ 'lightgray': '#E5E5E5', \ 'gray': '#E5E5E5', \ 'grey': '#E5E5E5', \ 'darkgrey': '#7F7F7F', \ 'darkgray': '#7F7F7F', \ 'blue': '#5C5CFF', \ 'lightblue': '#5C5CFF', \ 'green': '#00FF00', \ 'lightgreen': '#00FF00', \ 'cyan': '#00FFFF', \ 'lightcyan': '#00FFFF', \ 'red': '#FF0000', \ 'lightred': '#FF0000', \ 'magenta': '#FF00FF', \ 'lightmagenta': '#FF00FF', \ 'yellow': '#FFFF00', \ 'lightyellow': '#FFFF00', \ 'white': '#FFFFFF', \ } let s:w3c_color_names = { \ 'aliceblue': '#F0F8FF', \ 'antiquewhite': '#FAEBD7', \ 'aqua': '#00FFFF', \ 'aquamarine': '#7FFFD4', \ 'azure': '#F0FFFF', \ 'beige': '#F5F5DC', \ 'bisque': '#FFE4C4', \ 'black': '#000000', \ 'blanchedalmond': '#FFEBCD', \ 'blue': '#0000FF', \ 'blueviolet': '#8A2BE2', \ 'brown': '#A52A2A', \ 'burlywood': '#DEB887', \ 'cadetblue': '#5F9EA0', \ 'chartreuse': '#7FFF00', \ 'chocolate': '#D2691E', \ 'coral': '#FF7F50', \ 'cornflowerblue': '#6495ED', \ 'cornsilk': '#FFF8DC', \ 'crimson': '#DC143C', \ 'cyan': '#00FFFF', \ 'darkblue': '#00008B', \ 'darkcyan': '#008B8B', \ 'darkgoldenrod': '#B8860B', \ 'darkgray': '#A9A9A9', \ 'darkgreen': '#006400', \ 'darkkhaki': '#BDB76B', \ 'darkmagenta': '#8B008B', \ 'darkolivegreen': '#556B2F', \ 'darkorange': '#FF8C00', \ 'darkorchid': '#9932CC', \ 'darkred': '#8B0000', \ 'darksalmon': '#E9967A', \ 'darkseagreen': '#8FBC8F', \ 'darkslateblue': '#483D8B', \ 'darkslategray': '#2F4F4F', \ 'darkturquoise': '#00CED1', \ 'darkviolet': '#9400D3', \ 'deeppink': '#FF1493', \ 'deepskyblue': '#00BFFF', \ 'dimgray': '#696969', \ 'dodgerblue': '#1E90FF', \ 'firebrick': '#B22222', \ 'floralwhite': '#FFFAF0', \ 'forestgreen': '#228B22', \ 'fuchsia': '#FF00FF', \ 'gainsboro': '#DCDCDC', \ 'ghostwhite': '#F8F8FF', \ 'gold': '#FFD700', \ 'goldenrod': '#DAA520', \ 'gray': '#808080', \ 'green': '#008000', \ 'greenyellow': '#ADFF2F', \ 'honeydew': '#F0FFF0', \ 'hotpink': '#FF69B4', \ 'indianred': '#CD5C5C', \ 'indigo': '#4B0082', \ 'ivory': '#FFFFF0', \ 'khaki': '#F0E68C', \ 'lavender': '#E6E6FA', \ 'lavenderblush': '#FFF0F5', \ 'lawngreen': '#7CFC00', \ 'lemonchiffon': '#FFFACD', \ 'lightblue': '#ADD8E6', \ 'lightcoral': '#F08080', \ 'lightcyan': '#E0FFFF', \ 'lightgoldenrodyellow': '#FAFAD2', \ 'lightgray': '#D3D3D3', \ 'lightgreen': '#90EE90', \ 'lightpink': '#FFB6C1', \ 'lightsalmon': '#FFA07A', \ 'lightseagreen': '#20B2AA', \ 'lightskyblue': '#87CEFA', \ 'lightslategray': '#778899', \ 'lightsteelblue': '#B0C4DE', \ 'lightyellow': '#FFFFE0', \ 'lime': '#00FF00', \ 'limegreen': '#32CD32', \ 'linen': '#FAF0E6', \ 'magenta': '#FF00FF', \ 'maroon': '#800000', \ 'mediumaquamarine': '#66CDAA', \ 'mediumblue': '#0000CD', \ 'mediumorchid': '#BA55D3', \ 'mediumpurple': '#9370D8', \ 'mediumseagreen': '#3CB371', \ 'mediumslateblue': '#7B68EE', \ 'mediumspringgreen': '#00FA9A', \ 'mediumturquoise': '#48D1CC', \ 'mediumvioletred': '#C71585', \ 'midnightblue': '#191970', \ 'mintcream': '#F5FFFA', \ 'mistyrose': '#FFE4E1', \ 'moccasin': '#FFE4B5', \ 'navajowhite': '#FFDEAD', \ 'navy': '#000080', \ 'oldlace': '#FDF5E6', \ 'olive': '#808000', \ 'olivedrab': '#6B8E23', \ 'orange': '#FFA500', \ 'orangered': '#FF4500', \ 'orchid': '#DA70D6', \ 'palegoldenrod': '#EEE8AA', \ 'palegreen': '#98FB98', \ 'paleturquoise': '#AFEEEE', \ 'palevioletred': '#D87093', \ 'papayawhip': '#FFEFD5', \ 'peachpuff': '#FFDAB9', \ 'peru': '#CD853F', \ 'pink': '#FFC0CB', \ 'plum': '#DDA0DD', \ 'powderblue': '#B0E0E6', \ 'purple': '#800080', \ 'red': '#FF0000', \ 'rosybrown': '#BC8F8F', \ 'royalblue': '#4169E1', \ 'saddlebrown': '#8B4513', \ 'salmon': '#FA8072', \ 'sandybrown': '#F4A460', \ 'seagreen': '#2E8B57', \ 'seashell': '#FFF5EE', \ 'sienna': '#A0522D', \ 'silver': '#C0C0C0', \ 'skyblue': '#87CEEB', \ 'slateblue': '#6A5ACD', \ 'slategray': '#708090', \ 'snow': '#FFFAFA', \ 'springgreen': '#00FF7F', \ 'steelblue': '#4682B4', \ 'tan': '#D2B48C', \ 'teal': '#008080', \ 'thistle': '#D8BFD8', \ 'tomato': '#FF6347', \ 'turquoise': '#40E0D0', \ 'violet': '#EE82EE', \ 'wheat': '#F5DEB3', \ 'white': '#FFFFFF', \ 'whitesmoke': '#F5F5F5', \ 'yellow': '#FFFF00', \ 'yellowgreen': '#9ACD32' \ } " Returns an approximate grey index for the given grey level fun! s:grey_number(x) if &t_Co == 88 if a:x < 23 return 0 elseif a:x < 69 return 1 elseif a:x < 103 return 2 elseif a:x < 127 return 3 elseif a:x < 150 return 4 elseif a:x < 173 return 5 elseif a:x < 196 return 6 elseif a:x < 219 return 7 elseif a:x < 243 return 8 else return 9 endif else if a:x < 14 return 0 else let l:n = (a:x - 8) / 10 let l:m = (a:x - 8) % 10 if l:m < 5 return l:n else return l:n + 1 endif endif endif endfun " Returns the actual grey level represented by the grey index fun! s:grey_level(n) if &t_Co == 88 if a:n == 0 return 0 elseif a:n == 1 return 46 elseif a:n == 2 return 92 elseif a:n == 3 return 115 elseif a:n == 4 return 139 elseif a:n == 5 return 162 elseif a:n == 6 return 185 elseif a:n == 7 return 208 elseif a:n == 8 return 231 else return 255 endif else if a:n == 0 return 0 else return 8 + (a:n * 10) endif endif endfun " Returns the palette index for the given grey index fun! s:grey_colour(n) if &t_Co == 88 if a:n == 0 return 16 elseif a:n == 9 return 79 else return 79 + a:n endif else if a:n == 0 return 16 elseif a:n == 25 return 231 else return 231 + a:n endif endif endfun " Returns an approximate colour index for the given colour level fun! s:rgb_number(x) if &t_Co == 88 if a:x < 69 return 0 elseif a:x < 172 return 1 elseif a:x < 230 return 2 else return 3 endif else if a:x < 75 return 0 else let l:n = (a:x - 55) / 40 let l:m = (a:x - 55) % 40 if l:m < 20 return l:n else return l:n + 1 endif endif endif endfun " Returns the palette index for the given R/G/B colour indices fun! s:rgb_colour(x, y, z) if &t_Co == 88 return 16 + (a:x * 16) + (a:y * 4) + a:z else return 16 + (a:x * 36) + (a:y * 6) + a:z endif endfun " Returns the actual colour level for the given colour index fun! s:rgb_level(n) if &t_Co == 88 if a:n == 0 return 0 elseif a:n == 1 return 139 elseif a:n == 2 return 205 else return 255 endif else if a:n == 0 return 0 else return 55 + (a:n * 40) endif endif endfun " Returns the palette index to approximate the given R/G/B colour levels fun! s:colour(r, g, b) " Get the closest grey let l:gx = s:grey_number(a:r) let l:gy = s:grey_number(a:g) let l:gz = s:grey_number(a:b) " Get the closest colour let l:x = s:rgb_number(a:r) let l:y = s:rgb_number(a:g) let l:z = s:rgb_number(a:b) if l:gx == l:gy && l:gy == l:gz " There are two possibilities let l:dgr = s:grey_level(l:gx) - a:r let l:dgg = s:grey_level(l:gy) - a:g let l:dgb = s:grey_level(l:gz) - a:b let l:dgrey = (l:dgr * l:dgr) + (l:dgg * l:dgg) + (l:dgb * l:dgb) let l:dr = s:rgb_level(l:gx) - a:r let l:dg = s:rgb_level(l:gy) - a:g let l:db = s:rgb_level(l:gz) - a:b let l:drgb = (l:dr * l:dr) + (l:dg * l:dg) + (l:db * l:db) if l:dgrey < l:drgb " Use the grey return s:grey_colour(l:gx) else " Use the colour return s:rgb_colour(l:x, l:y, l:z) endif else " Only one possibility return s:rgb_colour(l:x, l:y, l:z) endif endfun function! coc#color#term2rgb(term) abort if a:term < 0 || a:term > 255 return '#000000' endif return s:xterm_colors[a:term] endfunction function! coc#color#rgb2term(rgb) let l:r = ("0x" . strpart(a:rgb, 0, 2)) + 0 let l:g = ("0x" . strpart(a:rgb, 2, 2)) + 0 let l:b = ("0x" . strpart(a:rgb, 4, 2)) + 0 return s:colour(l:r, l:g, l:b) endfunction function! coc#color#rgbToHex(...) let [r, g, b] = ( a:0==1 ? a:1 : a:000 ) let num = printf('%02x', float2nr(r)) . '' \ . printf('%02x', float2nr(g)) . '' \ . printf('%02x', float2nr(b)) . '' return '#' . num endfunction function! coc#color#hexToRgb(color) if type(a:color) == 2 let color = printf('%x', a:color) else let color = a:color end let matches = matchlist(color, s:patterns['hex']) let factor = 0x1 if empty(matches) let matches = matchlist(color, s:patterns['shortHex']) let factor = 0x10 end if len(matches) < 4 echohl Error echom 'Couldnt parse ' . string(color) . ' ' . string(matches) echohl None return end let r = str2nr(matches[1], 16) * factor let g = str2nr(matches[2], 16) * factor let b = str2nr(matches[3], 16) * factor return [r, g, b] endfunction function! coc#color#lighten(color, ...) let amount = a:0 ? \(type(a:1) < 2 ? \str2float(a:1) : a:1 ) \: 5 let rgb = coc#color#hexToRgb(a:color) let rgb = map(rgb, 'v:val + amount*(255 - v:val)/255') let rgb = map(rgb, 'v:val > 255.0 ? 255.0 : v:val') let rgb = map(rgb, 'float2nr(v:val)') let hex = coc#color#rgbToHex(rgb) return hex endfunction function! coc#color#darken(color, ...) let amount = a:0 ? \(type(a:1) < 2 ? \str2float(a:1) : a:1 ) \: 5.0 let rgb = coc#color#hexToRgb(a:color) let rgb = map(rgb, 'v:val - amount*v:val/255') let rgb = map(rgb, 'v:val < 0.0 ? 0.0 : v:val') let rgb = map(rgb, 'float2nr(v:val)') let hex = coc#color#rgbToHex(rgb) return hex endfu function! coc#color#luminance(rgb) abort let vals = [] for val in a:rgb let val = (val + 0.0)/255 if val <= 0.03928 call add(vals, val/12.92) else call add(vals, pow((val + 0.055)/1.055, 2.4)) endif endfor return vals[0] * 0.2126 + vals[1] * 0.7152 + vals[2] * 0.0722 endfunction function! coc#color#contrast(rgb1, rgb2) abort let lnum1 = coc#color#luminance(a:rgb1) let lnum2 = coc#color#luminance(a:rgb2) let brightest = lnum1 > lnum2 ? lnum1 : lnum2 let darkest = lnum1 < lnum2 ? lnum1 : lnum2 return (brightest + 0.05) / (darkest + 0.05) endfunction function! coc#color#hex_contrast(hex1, hex2) abort return coc#color#contrast(coc#color#hexToRgb(a:hex1), coc#color#hexToRgb(a:hex2)) endfunction function! coc#color#nameToHex(name, term) abort if a:term return has_key(s:xterm_16colors, a:name) ? s:xterm_16colors[a:name] : v:null endif return has_key(s:w3c_color_names, a:name) ? s:w3c_color_names[a:name] : v:null endfunction " [r, g, b] ['255', '255', '255'] " return ['65535', '65535', '65535'] or return v:false to cancel function! coc#color#pick_color(default_color) if has('mac') let default_color = map(a:default_color, {idx, val -> str2nr(val) * 65535 / 255 }) " This is the AppleScript magic: let ascrpt = ['-e "tell application \"' . s:app . '\""', \ '-e "' . s:activate . '"', \ "-e \"set AppleScript's text item delimiters to {\\\",\\\"}\"", \ '-e "set theColor to (choose color default color {' . default_color[0] . ", " . default_color[1] . ", " . default_color[2] . '}) as text"', \ '-e "' . s:quit . '"', \ '-e "end tell"', \ '-e "return theColor"'] let res = trim(system("osascript " . join(ascrpt, ' ') . " 2>/dev/null")) if empty(res) return v:false else return split(trim(res), ',') endif endif let hex_color = printf('#%02x%02x%02x', a:default_color[0], a:default_color[1], a:default_color[2]) if has('unix') if executable('zenity') let res = trim(system('zenity --title="Select a color" --color-selection --color="' . hex_color . '" 2> /dev/null')) if empty(res) return v:false else " res format is rgb(255,255,255) return map(split(res[4:-2], ','), {idx, val -> string(str2nr(trim(val)) * 65535 / 255)}) endif endif endif let rgb = v:false if !has('python') echohl Error | echom 'python support required, checkout :echo has(''python'')' | echohl None return endif try execute 'py import gtk' catch /.*/ echohl Error | echom 'python gtk module not found' | echohl None return endtry python << endpython import vim import gtk, sys # message strings wnd_title_insert = "Insert a color" csd = gtk.ColorSelectionDialog(wnd_title_insert) cs = csd.colorsel cs.set_current_color(gtk.gdk.color_parse(vim.eval("hex_color"))) cs.set_current_alpha(65535) cs.set_has_opacity_control(False) # cs.set_has_palette(int(vim.eval("s:display_palette"))) if csd.run()==gtk.RESPONSE_OK: c = cs.get_current_color() s = [str(int(c.red)),',',str(int(c.green)),',',str(int(c.blue))] thecolor = ''.join(s) vim.command(":let rgb = split('%s',',')" % thecolor) csd.destroy() endpython return rgb endfunction ================================================ FILE: autoload/coc/compat.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') " first window id for bufnr " builtin bufwinid returns window of current tab only function! coc#compat#buf_win_id(bufnr) abort return get(win_findbuf(a:bufnr), 0, -1) endfunction function! coc#compat#buf_set_lines(bufnr, start, end, replacement) abort if bufloaded(a:bufnr) call coc#compat#call('buf_set_lines', [a:bufnr, a:start, a:end, 0, a:replacement]) endif endfunction function! coc#compat#buf_line_count(bufnr) abort if !bufloaded(a:bufnr) return 0 endif return coc#compat#call('buf_line_count', [a:bufnr]) endfunction " remove keymap for bufnr, not throw error function! coc#compat#buf_del_keymap(bufnr, mode, lhs) abort if a:bufnr != 0 && !bufexists(a:bufnr) return endif try call coc#compat#call('buf_del_keymap', [a:bufnr, a:mode, a:lhs]) catch /E31/ " ignore keymap doesn't exist endtry endfunction function! coc#compat#buf_add_keymap(bufnr, mode, lhs, rhs, opts) abort if a:bufnr != 0 && !bufexists(a:bufnr) return endif call coc#compat#call('buf_set_keymap', [a:bufnr, a:mode, a:lhs, a:rhs, a:opts]) endfunction function! coc#compat#clear_matches(winid) abort silent! call clearmatches(a:winid) endfunction function! coc#compat#matchaddpos(group, pos, priority, winid) abort let curr = win_getid() if curr == a:winid call matchaddpos(a:group, a:pos, a:priority, -1) else call matchaddpos(a:group, a:pos, a:priority, -1, {'window': a:winid}) endif endfunction " hlGroup, pos, priority function! coc#compat#matchaddgroups(winid, groups) abort for group in a:groups call matchaddpos(group['hlGroup'], [group['pos']], group['priority'], -1, {'window': a:winid}) endfor endfunction " Delete var, not throw version. function! coc#compat#del_var(name) abort if s:is_vim execute 'unlet! g:' . a:name else silent! call nvim_del_var(a:name) endif endfunction function! coc#compat#tabnr_id(tabnr) abort if s:is_vim return coc#api#TabNrId(a:tabnr) endif return nvim_list_tabpages()[a:tabnr - 1] endfunction function! coc#compat#list_runtime_paths() abort return coc#compat#call('list_runtime_paths', []) endfunction function! coc#compat#buf_execute(bufnr, cmds, ...) abort let silent = get(a:, 1, 'silent') if s:is_vim let cmds = copy(a:cmds)->map({_, val -> 'legacy ' . val}) call coc#api#BufExecute(a:bufnr, cmds, silent) else endif endfunction function coc#compat#execute(command, ...) abort return execute(a:command, get(a:, 1, 'silent')) endfunction function! coc#compat#eval(expr) abort return eval(a:expr) endfunction function coc#compat#win_execute(id, command, ...) abort return win_execute(a:id, a:command, get(a:, 1, 'silent')) endfunction " call api function on vim or neovim function! coc#compat#call(fname, args) abort if s:is_vim return call('coc#api#' . toupper(a:fname[0]) . a:fname[1:], a:args) endif return call('nvim_' . a:fname, a:args) endfunction function! coc#compat#send_error(fname, tracestack) abort let msg = v:exception .. ' - on "' .. a:fname .. '"' if a:tracestack let msg = msg .. " \n" .. v:throwpoint endif call coc#rpc#notify('nvim_error_event', [0, msg]) endfunction " vim: set sw=2 ts=2 sts=2 et tw=78 foldlevel=0: ================================================ FILE: autoload/coc/cursor.vim ================================================ scriptencoding utf-8 " Position of cursor relative to screen cell function! coc#cursor#screen_pos() abort let nr = winnr() let [row, col] = win_screenpos(nr) return [row + winline() - 2, col + wincol() - 2] endfunction function! coc#cursor#move_by_col(delta) let pos = getcurpos() call cursor(pos[1], pos[2] + a:delta) endfunction " Get cursor position. function! coc#cursor#position() let line = getline('.') return [line('.') - 1, coc#string#character_index(line, col('.') - 1)] endfunction " Move cursor to position. function! coc#cursor#move_to(line, character) abort let content = getline(a:line + 1) call cursor(a:line + 1, coc#string#byte_index(content, a:character) + 1) endfunction " Character offset of current cursor, vim provide bytes offset only. function! coc#cursor#char_offset() abort let offset = 0 let lnum = line('.') for i in range(1, lnum) if i == lnum let offset += strchars(strpart(getline('.'), 0, col('.')-1)) else let offset += strchars(getline(i)) + 1 endif endfor return offset endfunction " Returns latest selection range function! coc#cursor#get_selection(char) abort let m = a:char ? 'char' : visualmode() if empty(m) return v:null endif let [_, sl, sc, soff] = getpos(m ==# 'char' ? "'[" : "'<") let [_, el, ec, eoff] = getpos(m ==# 'char' ? "']" : "'>") let start_idx = coc#string#character_index(getline(sl), sc - 1) if m ==# 'V' return [sl - 1, start_idx, el, 0] endif let line = getline(el) let end_idx = coc#string#character_index(line, ec - 1) if m !=# 'char' if &selection ==# 'exclusive' && !(sl == el && start_idx == end_idx) let end_idx = end_idx - 1 endif let end_idx = end_idx == coc#string#character_length(line) ? end_idx : end_idx + 1 endif return [sl - 1, start_idx, el - 1, end_idx] endfunction ================================================ FILE: autoload/coc/dialog.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:root = expand(':h:h:h') let s:prompt_win_bufnr = 0 let s:list_win_bufnr = 0 let s:prompt_win_width = get(g:, 'coc_prompt_win_width', 32) let s:frames = ['· ', '·· ', '···', ' ··', ' ·', ' '] let s:sign_group = 'PopUpCocDialog' let s:detail_bufnr = 0 let s:term_support = s:is_vim ? has('terminal') : 1 " Float window aside pum function! coc#dialog#create_pum_float(lines, config) abort let winid = coc#float#get_float_by_kind('pumdetail') if empty(a:lines) || !coc#pum#visible() if winid call coc#float#close(winid) endif return endif let pumbounding = coc#pum#info() let border = get(a:config, 'border', []) let pw = pumbounding['width'] + (pumbounding['border'] ? 0 : get(pumbounding, 'scrollbar', 0)) let rp = &columns - pumbounding['col'] - pw let showRight = pumbounding['col'] > rp ? 0 : 1 let maxWidth = showRight ? min([rp - 1, a:config['maxWidth']]) : min([pumbounding['col'] - 1, a:config['maxWidth']]) let bh = get(border, 0 ,0) + get(border, 2, 0) let maxHeight = &lines - pumbounding['row'] - &cmdheight - 1 - bh if maxWidth <= 2 || maxHeight < 1 return v:null endif let width = 0 for line in a:lines let dw = max([1, strdisplaywidth(line)]) let width = max([width, dw + 2]) endfor let width = width < maxWidth ? width : maxWidth let ch = coc#string#content_height(a:lines, width - 2) let height = ch < maxHeight ? ch : maxHeight let lines = map(a:lines, {_, s -> s =~# '^─' ? repeat('─', width - 2 + (s:is_vim && ch > height ? -1 : 0)) : s}) let opts = { \ 'lines': lines, \ 'highlights': get(a:config, 'highlights', []), \ 'relative': 'editor', \ 'col': showRight ? pumbounding['col'] + pw : pumbounding['col'] - width, \ 'row': pumbounding['row'], \ 'height': height, \ 'width': width - 2 + (s:is_vim && ch > height ? -1 : 0), \ 'scrollinside': showRight ? 0 : 1, \ 'codes': get(a:config, 'codes', []), \ } for key in ['border', 'highlight', 'borderhighlight', 'winblend', 'focusable', 'shadow', 'rounded', 'title'] if has_key(a:config, key) let opts[key] = a:config[key] endif endfor call s:close_auto_hide_wins(winid) let result = coc#float#create_float_win(winid, s:detail_bufnr, opts) if empty(result) return endif let s:detail_bufnr = result[1] call setwinvar(result[0], 'kind', 'pumdetail') if !s:is_vim call coc#float#nvim_scrollbar(result[0]) endif endfunction " Float window below/above cursor function! coc#dialog#create_cursor_float(winid, bufnr, lines, config) abort if coc#prompt#activated() return v:null endif let pumAlignTop = get(a:config, 'pumAlignTop', 0) let modes = get(a:config, 'modes', ['n', 'i', 'ic', 's']) let mode = mode() let currbuf = bufnr('%') let pos = [line('.'), col('.')] if index(modes, mode) == -1 return v:null endif let dimension = coc#dialog#get_config_cursor(a:lines, a:config) if empty(dimension) return v:null endif if coc#pum#visible() && ((pumAlignTop && dimension['row'] <0)|| (!pumAlignTop && dimension['row'] > 0)) return v:null endif let width = dimension['width'] let lines = map(a:lines, {_, s -> s =~# '^─' ? repeat('─', width) : s}) let config = extend(extend({'lines': lines, 'relative': 'cursor'}, a:config), dimension) call s:close_auto_hide_wins(a:winid) let res = coc#float#create_float_win(a:winid, a:bufnr, config) if empty(res) return v:null endif let alignTop = dimension['row'] < 0 let winid = res[0] let bufnr = res[1] call win_execute(winid, 'setl nonumber') if s:is_vim call timer_start(0, { -> execute('redraw')}) else redraw call coc#float#nvim_scrollbar(winid) endif return [currbuf, pos, winid, bufnr, alignTop] endfunction " Use terminal buffer function! coc#dialog#_create_prompt_vim(title, default, opts) abort execute 'hi link CocPopupTerminal '.get(a:opts, 'highlight', 'CocFloating') let node = expand(get(g:, 'coc_node_path', 'node')) let placeHolder = get(a:opts, 'placeHolder', '') let opt = { \ 'term_rows': 1, \ 'hidden': 1, \ 'term_finish': 'close', \ 'norestore': 1, \ 'tty_type': 'conpty', \ 'term_highlight': 'CocPopupTerminal' \ } let bufnr = term_start([node, s:root . '/bin/prompt.js', a:default, empty(placeHolder) ? '' : placeHolder], opt) call term_setapi(bufnr, 'Coc') call setbufvar(bufnr, 'current', type(a:default) == v:t_string ? a:default : '') let res = s:create_prompt_win(bufnr, a:title, a:default, a:opts) if empty(res) return endif let winid = res[0] " call win_gotoid(winid) call coc#util#do_autocmd('CocOpenFloatPrompt') let pos = popup_getpos(winid) " width height row col let dimension = [pos['width'], pos['height'], pos['line'] - 1, pos['col'] - 1] return [bufnr, winid, dimension] endfunction " Use normal buffer on neovim function! coc#dialog#_create_prompt_nvim(title, default, opts) abort let result = s:create_prompt_win(s:prompt_win_bufnr, a:title, a:default, a:opts) if empty(result) return endif let winid = result[0] let s:prompt_win_bufnr = result[1] let bufnr = s:prompt_win_bufnr call sign_unplace(s:sign_group, { 'buffer': s:prompt_win_bufnr }) call nvim_set_current_win(winid) inoremap inoremap pumvisible() ? "\" : "\" exe 'imap ' exe 'nnoremap :call coc#float#close('.winid.')' exe 'inoremap "\=coc#dialog#prompt_insert()\\"' if get(a:opts, 'list', 0) for key in ['', '', '', '', '', '', '', '', ''] let escaped = key ==# '' ? '\' : substitute(key, '\(<\|>\)', '\\\1', 'g') exe 'inoremap '.key.' call coc#rpc#notify("PromptKeyPress", ['.bufnr.', "'.escaped.'"])' endfor endif let mode = mode() if mode ==# 'n' call feedkeys('A', 'int') elseif mode ==# 'i' call feedkeys("\", 'int') else call feedkeys("\A", 'int') endif let placeHolder = get(a:opts, 'placeHolder', '') if empty(a:default) && !empty(placeHolder) let src_id = coc#highlight#create_namespace('input-box') call nvim_buf_set_extmark(bufnr, src_id, 0, 0, { \ 'virt_text': [[placeHolder, 'CocInputBoxVirtualText']], \ 'virt_text_pos': 'overlay', \ }) endif call coc#util#do_autocmd('CocOpenFloatPrompt') let id = coc#float#get_related(winid, 'border') let pos = nvim_win_get_position(id) let dimension = [nvim_win_get_width(id), nvim_win_get_height(id), pos[0], pos[1]] return [bufnr, winid, dimension] endfunction " Create float window for input function! coc#dialog#create_prompt_win(title, default, opts) abort call s:close_auto_hide_wins() if s:is_vim if !s:term_support " use popup_menu or inputlist instead let pickItems = get(a:opts, 'quickpick', []) if len(pickItems) > 0 call coc#ui#quickpick(a:title, pickItems, {err, res -> s:on_quickpick_selected(err, res)}) else call inputsave() let value = input(a:title.':', a:default) call inputrestore() if empty(value) " Cancel call timer_start(50, { -> coc#rpc#notify('CocAutocmd', ['BufWinLeave', -1, -1])}) else call timer_start(50, { -> coc#rpc#notify('PromptInsert', [value, -1])}) endif endif return [-1, -1, [0, 0, 0, 0]] endif return coc#dialog#_create_prompt_vim(a:title, a:default, a:opts) endif return coc#dialog#_create_prompt_nvim(a:title, a:default, a:opts) endfunction " Create list window under target window function! coc#dialog#create_list(target, dimension, opts) abort if a:target < 0 return [-1, -1] endif let maxHeight = get(a:opts, 'maxHeight', 30) let height = get(a:opts, 'linecount', 1) let height = min([maxHeight, height, &lines - &cmdheight - 1 - a:dimension['row'] + a:dimension['height']]) let chars = get(a:opts, 'rounded', 1) ? ['╯', '╰'] : ['┘', '└'] let width = a:dimension['width'] - 2 let config = extend(copy(a:opts), { \ 'relative': 'editor', \ 'row': a:dimension['row'] + a:dimension['height'], \ 'col': a:dimension['col'], \ 'width': width, \ 'height': height, \ 'border': [1, 1, 1, 1], \ 'scrollinside': 1, \ 'borderchars': extend(['─', '│', '─', '│', '├', '┤'], chars) \ }) let bufnr = 0 let result = coc#float#create_float_win(0, s:list_win_bufnr, config) if empty(result) return endif let winid = result[0] call coc#float#add_related(winid, a:target) call setwinvar(winid, 'auto_height', get(a:opts, 'autoHeight', 1)) call setwinvar(winid, 'core_width', width) call setwinvar(winid, 'max_height', maxHeight) call setwinvar(winid, 'target_winid', a:target) call setwinvar(winid, 'kind', 'list') call coc#dialog#check_scroll_vim(a:target) return result endfunction " Create menu picker for pick single item function! coc#dialog#create_menu(lines, config) abort call s:close_auto_hide_wins() let highlight = get(a:config, 'highlight', 'CocFloating') let borderhighlight = get(a:config, 'borderhighlight', [highlight]) let relative = get(a:config, 'relative', 'cursor') let lines = a:lines let content = get(a:config, 'content', '') let maxWidth = get(a:config, 'maxWidth', 80) let highlights = get(a:config, 'highlights', []) let contentCount = 0 if !empty(content) let contentLines = coc#string#reflow(split(content, '\r\?\n'), maxWidth) let contentCount = len(contentLines) let lines = extend(contentLines, lines) if !empty(highlights) for item in highlights let item['lnum'] = item['lnum'] + contentCount endfor endif endif let opts = { \ 'lines': lines, \ 'highlight': highlight, \ 'title': get(a:config, 'title', ''), \ 'borderhighlight': borderhighlight, \ 'maxWidth': maxWidth, \ 'maxHeight': get(a:config, 'maxHeight', 80), \ 'rounded': get(a:config, 'rounded', 0), \ 'border': [1, 1, 1, 1], \ 'highlights': highlights, \ 'relative': relative, \ } if relative ==# 'editor' let dimension = coc#dialog#get_config_editor(lines, opts) else let dimension = coc#dialog#get_config_cursor(lines, opts) endif call extend(opts, dimension) let ids = coc#float#create_float_win(0, s:prompt_win_bufnr, opts) if empty(ids) return endif let s:prompt_win_bufnr = ids[1] call coc#dialog#set_cursor(ids[0], ids[1], contentCount + 1) redraw if !s:is_vim call coc#float#nvim_scrollbar(ids[0]) endif return [ids[0], ids[1], contentCount] endfunction " Create dialog at center of screen function! coc#dialog#create_dialog(lines, config) abort call s:close_auto_hide_wins() " dialog always have borders let title = get(a:config, 'title', '') let buttons = get(a:config, 'buttons', []) let highlight = get(a:config, 'highlight', 'CocFloating') let borderhighlight = get(a:config, 'borderhighlight', [highlight]) let opts = { \ 'title': title, \ 'rounded': get(a:config, 'rounded', 0), \ 'relative': 'editor', \ 'border': [1,1,1,1], \ 'close': get(a:config, 'close', 1), \ 'highlight': highlight, \ 'highlights': get(a:config, 'highlights', []), \ 'buttons': buttons, \ 'borderhighlight': borderhighlight, \ 'getchar': get(a:config, 'getchar', 0) \ } call extend(opts, coc#dialog#get_config_editor(a:lines, a:config)) let bufnr = coc#float#create_buf(0, a:lines) let res = coc#float#create_float_win(0, bufnr, opts) if empty(res) return endif if get(a:config, 'cursorline', 0) call coc#dialog#place_sign(bufnr, 1) endif if !s:is_vim redraw call coc#float#nvim_scrollbar(res[0]) endif return res endfunction function! coc#dialog#prompt_confirm(title, cb) abort call s:close_auto_hide_wins() if s:is_vim && exists('*popup_dialog') try call popup_dialog(a:title. ' (y/n)?', { \ 'highlight': 'Normal', \ 'filter': 'popup_filter_yesno', \ 'callback': {id, res -> a:cb(v:null, res)}, \ 'borderchars': get(g:, 'coc_borderchars', ['─', '│', '─', '│', '╭', '╮', '╯', '╰']), \ 'borderhighlight': ['MoreMsg'] \ }) catch /.*/ call a:cb(v:exception) endtry return endif let text = ' '. a:title . ' (y/n)? ' let maxWidth = coc#math#min(78, &columns - 2) let width = coc#math#min(maxWidth, strdisplaywidth(text)) let maxHeight = &lines - &cmdheight - 1 let height = coc#math#min(maxHeight, float2nr(ceil(str2float(string(strdisplaywidth(text)))/width))) let arr = coc#float#create_float_win(0, s:prompt_win_bufnr, { \ 'col': &columns/2 - width/2 - 1, \ 'row': maxHeight/2 - height/2 - 1, \ 'width': width, \ 'height': height, \ 'border': [1,1,1,1], \ 'focusable': v:false, \ 'relative': 'editor', \ 'highlight': 'Normal', \ 'borderhighlight': 'MoreMsg', \ 'style': 'minimal', \ 'lines': [text], \ }) if empty(arr) call a:cb('Window create failed!') return endif let winid = arr[0] let s:prompt_win_bufnr = arr[1] call sign_unplace(s:sign_group, { 'buffer': s:prompt_win_bufnr }) let res = 0 redraw " same result as vim while 1 let key = nr2char(getchar()) if key == "\" let res = -1 break elseif key == "\" || key == 'n' || key == 'N' let res = 0 break elseif key == 'y' || key == 'Y' let res = 1 break endif endw call coc#float#close(winid) call a:cb(v:null, res) endfunction " works on neovim only function! coc#dialog#get_prompt_win() abort if s:prompt_win_bufnr == 0 return -1 endif return get(win_findbuf(s:prompt_win_bufnr), 0, -1) endfunction function! coc#dialog#get_config_editor(lines, config) abort let title = get(a:config, 'title', '') let maxheight = min([get(a:config, 'maxHeight', 78), &lines - &cmdheight - 6]) let maxwidth = min([get(a:config, 'maxWidth', 78), &columns - 2]) let buttons = get(a:config, 'buttons', []) let minwidth = s:min_btns_width(buttons) if maxheight <= 0 || maxwidth <= 0 || minwidth > maxwidth throw 'Not enough spaces for float window' endif let ch = 0 let width = min([strdisplaywidth(title) + 1, maxwidth]) for line in a:lines let dw = max([1, strdisplaywidth(line)]) if dw < maxwidth && dw > width let width = dw elseif dw >= maxwidth let width = maxwidth endif let ch += float2nr(ceil(str2float(string(dw))/maxwidth)) endfor let width = max([minwidth, width]) let height = coc#math#min(ch ,maxheight) return { \ 'row': &lines/2 - (height + 4)/2, \ 'col': &columns/2 - (width + 2)/2, \ 'width': width, \ 'height': height, \ } endfunction function! coc#dialog#prompt_insert() abort let value = getline('.') call coc#rpc#notify('PromptInsert', [value, bufnr('%')]) return '' endfunction " Dimension of window with lines relative to cursor " Width & height excludes border & padding function! coc#dialog#get_config_cursor(lines, config) abort let preferTop = get(a:config, 'preferTop', 0) let title = get(a:config, 'title', '') let border = get(a:config, 'border', []) if empty(border) && len(title) let border = [1, 1, 1, 1] endif let bh = get(border, 0, 0) + get(border, 2, 0) let vh = &lines - &cmdheight - 1 if vh <= 0 return v:null endif let maxWidth = coc#math#min(get(a:config, 'maxWidth', &columns - 1), &columns - 1) if maxWidth < 3 return v:null endif let maxHeight = coc#math#min(get(a:config, 'maxHeight', vh), vh) let width = coc#math#min(40, strdisplaywidth(title)) + 3 for line in a:lines let dw = max([1, strdisplaywidth(line)]) let width = max([width, dw + 2]) endfor let width = coc#math#min(maxWidth, width) let ch = coc#string#content_height(a:lines, width - 2) let [lineIdx, colIdx] = coc#cursor#screen_pos() " How much we should move left let offsetX = coc#math#min(get(a:config, 'offsetX', 0), colIdx) let showTop = 0 let hb = vh - lineIdx -1 if lineIdx > bh + 2 && (preferTop || (lineIdx > hb && hb < ch + bh)) let showTop = 1 endif let height = coc#math#min(maxHeight, ch + bh, showTop ? lineIdx - 1 : hb) if height <= bh return v:null endif let col = - max([offsetX, colIdx - (&columns - 1 - width)]) let row = showTop ? - height + bh : 1 return { \ 'row': row, \ 'col': col, \ 'width': width - 2, \ 'height': height - bh \ } endfunction function! coc#dialog#change_border_hl(winid, hlgroup) abort if !hlexists(a:hlgroup) return endif if s:is_vim if coc#float#valid(a:winid) call popup_setoptions(a:winid, {'borderhighlight': repeat([a:hlgroup], 4)}) redraw endif else let winid = coc#float#get_related(a:winid, 'border') if winid > 0 call setwinvar(winid, '&winhl', 'Normal:'.a:hlgroup) endif endif endfunction function! coc#dialog#change_title(winid, title) abort if s:is_vim if coc#float#valid(a:winid) call popup_setoptions(a:winid, {'title': a:title}) redraw endif else let winid = coc#float#get_related(a:winid, 'border') if winid > 0 let bufnr = winbufnr(winid) let line = getbufline(bufnr, 1)[0] let top = strcharpart(line, 0, 1) \.repeat('─', strchars(line) - 2) \.strcharpart(line, strchars(line) - 1, 1) if !empty(a:title) let top = coc#string#compose(top, 1, a:title.' ') endif call nvim_buf_set_lines(bufnr, 0, 1, v:false, [top]) endif endif endfunction function! coc#dialog#change_input_value(winid, bufnr, value) abort if !coc#float#valid(a:winid) return endif if win_getid() != a:winid call win_gotoid(a:winid) endif if s:is_vim if !s:term_support call term_sendkeys(a:bufnr, "\\".a:value) endif " call timer_start(3000, { -> term_sendkeys(bufnr, "\\abcd")}) else let mode = mode() if mode ==# 'i' call feedkeys("\", 'int') else call feedkeys("\A", 'int') endif " Use complete to replace text before let saved_completeopt = &completeopt if saved_completeopt =~ 'menuone' noa set completeopt=menu endif noa call complete(1, [{ 'empty': 1, 'word': a:value }]) call feedkeys("\\", 'in') execute 'noa set completeopt='.saved_completeopt endif endfunction function! coc#dialog#change_loading(winid, loading) abort if coc#float#valid(a:winid) let winid = coc#float#get_related(a:winid, 'loading') if !a:loading && winid > 0 call coc#float#close(winid) endif if a:loading && winid == 0 let bufnr = s:create_loading_buf() if s:is_vim let pos = popup_getpos(a:winid) let winid = popup_create(bufnr, { \ 'line': pos['line'] + 1, \ 'col': pos['col'] + pos['width'] - 4, \ 'maxheight': 1, \ 'maxwidth': 3, \ 'zindex': 999, \ 'highlight': get(popup_getoptions(a:winid), 'highlight', 'CocFloating') \ }) else let pos = nvim_win_get_position(a:winid) let width = nvim_win_get_width(a:winid) let opts = { \ 'relative': 'editor', \ 'row': pos[0], \ 'col': pos[1] + width - 3, \ 'focusable': v:false, \ 'width': 3, \ 'height': 1, \ 'style': 'minimal', \ 'zindex': 900, \ } let winid = nvim_open_win(bufnr, v:false, opts) call setwinvar(winid, '&winhl', getwinvar(a:winid, '&winhl')) endif call setwinvar(winid, 'kind', 'loading') call setbufvar(bufnr, 'target_winid', a:winid) call setbufvar(bufnr, 'popup', winid) call coc#float#add_related(winid, a:winid) endif endif endfunction " Update list with new lines and highlights function! coc#dialog#update_list(winid, bufnr, lines, highlights) abort if coc#window#tabnr(a:winid) == tabpagenr() if getwinvar(a:winid, 'auto_height', 0) let row = coc#float#get_row(a:winid) let width = getwinvar(a:winid, 'core_width', 80) let height = s:get_height(a:lines, width) let height = min([getwinvar(a:winid, 'max_height', 10), height, &lines - &cmdheight - 1 - row]) let curr = s:is_vim ? popup_getpos(a:winid)['core_height'] : nvim_win_get_height(a:winid) let delta = height - curr if delta != 0 call coc#float#change_height(a:winid, delta) endif endif call coc#compat#buf_set_lines(a:bufnr, 0, -1, a:lines) call coc#highlight#add_highlights(a:winid, [], a:highlights) if s:is_vim let target = getwinvar(a:winid, 'target_winid', -1) if target != -1 call coc#dialog#check_scroll_vim(target) endif call win_execute(a:winid, 'exe 1') endif endif endfunction " Fix width of prompt window same as list window on scrollbar change function! coc#dialog#check_scroll_vim(winid) abort if s:is_vim && coc#float#valid(a:winid) let winid = coc#float#get_related(a:winid, 'list') if winid redraw let pos = popup_getpos(winid) let width = pos['width'] + (pos['scrollbar'] ? 1 : 0) if width != popup_getpos(a:winid)['width'] call popup_move(a:winid, { \ 'minwidth': width - 2, \ 'maxwidth': width - 2, \ }) endif endif endif endfunction function! coc#dialog#set_cursor(winid, bufnr, line) abort if a:winid >= 0 if s:is_vim call win_execute(a:winid, 'exe ' . max([a:line, 1]), 'silent!') call popup_setoptions(a:winid, {'cursorline' : 1}) call popup_setoptions(a:winid, {'cursorline' : 0}) else call nvim_win_set_cursor(a:winid, [max([a:line, 1]), 0]) endif call coc#dialog#place_sign(a:bufnr, a:line) endif endfunction function! coc#dialog#place_sign(bufnr, line) abort call sign_unplace(s:sign_group, { 'buffer': a:bufnr }) if a:line > 0 call sign_place(6, s:sign_group, 'CocCurrentLine', a:bufnr, {'lnum': a:line}) endif endfunction function! s:create_prompt_win(bufnr, title, default, opts) abort let config = s:get_prompt_dimension(a:title, a:default, a:opts) return coc#float#create_float_win(0, a:bufnr, extend(config, { \ 'style': 'minimal', \ 'border': get(a:opts, 'border', [1,1,1,1]), \ 'rounded': get(a:opts, 'rounded', 1), \ 'prompt': 1, \ 'title': a:title, \ 'lines': s:is_vim ? v:null : [a:default], \ 'highlight': get(a:opts, 'highlight', 'CocFloating'), \ 'borderhighlight': [get(a:opts, 'borderhighlight', 'CocFloatBorder')], \ })) endfunction " Could be center(with optional marginTop) or cursor function! s:get_prompt_dimension(title, default, opts) abort let relative = get(a:opts, 'position', 'cursor') ==# 'cursor' ? 'cursor' : 'editor' let curr = win_screenpos(winnr())[1] + wincol() - 2 let minWidth = get(a:opts, 'minWidth', s:prompt_win_width) let width = min([max([strwidth(a:default) + 2, strwidth(a:title) + 2, minWidth]), &columns - 2]) if get(a:opts, 'maxWidth', 0) let width = min([width, a:opts['maxWidth']]) endif if relative ==# 'cursor' let [lineIdx, colIdx] = coc#cursor#screen_pos() if width == &columns - 2 let col = 0 - curr else let col = curr + width <= &columns - 2 ? 0 : curr + width - &columns + 2 endif let config = { \ 'row': lineIdx == 0 ? 1 : 0, \ 'col': colIdx == 0 ? 0 : col - 1, \ } else let marginTop = get(a:opts, 'marginTop', v:null) if marginTop is v:null let row = (&lines - &cmdheight - 2) / 2 else let row = marginTop < 2 ? 1 : min([marginTop, &columns - &cmdheight]) endif let config = { \ 'col': float2nr((&columns - width) / 2), \ 'row': row - s:is_vim, \ } endif return extend(config, {'relative': relative, 'width': width, 'height': 1}) endfunction function! s:min_btns_width(buttons) abort if empty(a:buttons) return 0 endif let minwidth = len(a:buttons)*3 - 1 for txt in a:buttons let minwidth = minwidth + strdisplaywidth(txt) endfor return minwidth endfunction " Close windows that should auto hide function! s:close_auto_hide_wins(...) abort let winids = coc#float#get_float_win_list() let except = get(a:, 1, 0) for id in winids if except && id == except continue endif if getwinvar(id, 'autohide', 0) call coc#float#close(id) endif endfor endfunction function! s:create_loading_buf() abort let bufnr = coc#float#create_buf(0) call s:change_loading_buf(bufnr, 0) return bufnr endfunction function! s:get_height(lines, width) abort let height = 0 for line in a:lines let height += float2nr(strdisplaywidth(line) / a:width) + 1 endfor return max([1, height]) endfunction function! s:change_loading_buf(bufnr, idx) abort if bufloaded(a:bufnr) let target = getbufvar(a:bufnr, 'target_winid', v:null) if !empty(target) && !coc#float#valid(target) call coc#float#close(getbufvar(a:bufnr, 'popup')) return endif let line = get(s:frames, a:idx, ' ') call setbufline(a:bufnr, 1, line) call coc#highlight#add_highlight(a:bufnr, -1, 'CocNotificationProgress', 0, 0, -1) let idx = a:idx == len(s:frames) - 1 ? 0 : a:idx + 1 call timer_start(100, { -> s:change_loading_buf(a:bufnr, idx)}) endif endfunction function! s:on_quickpick_selected(errMsg, res) abort if !empty(a:errMsg) throw a:errMsg endif call timer_start(50, { -> coc#rpc#notify('InputListSelect', [a:res - 1])}) endfunction ================================================ FILE: autoload/coc/dict.vim ================================================ scriptencoding utf-8 function! coc#dict#equal(one, two) abort for key in keys(a:one) if a:one[key] != a:two[key] return 0 endif endfor return 1 endfunction " Return new dict with keys removed function! coc#dict#omit(dict, keys) abort let res = {} for key in keys(a:dict) if index(a:keys, key) == -1 let res[key] = a:dict[key] endif endfor return res endfunction " Return new dict with keys only function! coc#dict#pick(dict, keys) abort let res = {} for key in keys(a:dict) if index(a:keys, key) != -1 let res[key] = a:dict[key] endif endfor return res endfunction ================================================ FILE: autoload/coc/float.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:borderchars = get(g:, 'coc_borderchars', ['─', '│', '─', '│', '┌', '┐', '┘', '└']) let s:rounded_borderchars = s:borderchars[0:3] + ['╭', '╮', '╯', '╰'] let s:borderjoinchars = get(g:, 'coc_border_joinchars', ['┬', '┤', '┴', '├']) let s:pad_bufnr = -1 " Check visible float/popup exists. function! coc#float#has_float(...) abort return len(coc#float#get_float_win_list(get(a:, 1, 0))) > 0 endfunction function! coc#float#close_all(...) abort let winids = coc#float#get_float_win_list(get(a:, 1, 0)) for id in winids try call coc#float#close(id) catch /E5555:/ " ignore endtry endfor return '' endfunction function! coc#float#jump() abort if !s:is_vim let winids = coc#float#get_float_win_list() if !empty(winids) call win_gotoid(winids[0]) endif endif endfunction function! coc#float#valid(winid) abort if a:winid <= 0 return 0 endif if !s:is_vim if !nvim_win_is_valid(a:winid) return 0 endif return !empty(nvim_win_get_config(a:winid)['relative']) endif try return !empty(popup_getpos(a:winid)) catch /^Vim\%((\a\+)\)\=:E993/ " not a popup window return 0 endtry endfunction function! coc#float#get_height(winid) abort if !s:is_vim let borderwin = coc#float#get_related(a:winid, 'border') if borderwin return nvim_win_get_height(borderwin) endif return nvim_win_get_height(a:winid) endif return get(popup_getpos(a:winid), 'height', 0) endfunction function! coc#float#change_height(winid, delta) abort if s:is_vim let curr = get(popup_getpos(a:winid), 'core_height', v:null) if curr isnot v:null call popup_move(a:winid, { \ 'maxheight': max([1, curr + a:delta]), \ 'minheight': max([1, curr + a:delta]), \ }) endif else let winids = copy(coc#window#get_var(a:winid, 'related', [])) call filter(winids, 'index(["border","pad","scrollbar"],coc#window#get_var(v:val,"kind","")) >= 0') call add(winids, a:winid) for winid in winids if coc#window#get_var(winid, 'kind', '') ==# 'border' let bufnr = winbufnr(winid) if a:delta > 0 call appendbufline(bufnr, 1, repeat(getbufline(bufnr, 2), a:delta)) else call deletebufline(bufnr, 2, 2 - a:delta - 1) endif endif let height = nvim_win_get_height(winid) call nvim_win_set_height(winid, max([1, height + a:delta])) endfor endif endfunction " create or config float window, returns [winid, bufnr], config including: " - relative: could be 'editor' 'cursor' " - row: line count relative to editor/cursor, nagetive number means abover cursor. " - col: column count relative to editor/cursor, nagetive number means left of cursor. " - width: content width without border and title. " - height: content height without border and title. " - lines: (optional) lines to insert, default to v:null. " - title: (optional) title. " - border: (optional) border as number list, like [1, 1, 1 ,1]. " - cursorline: (optional) enable cursorline when is 1. " - autohide: (optional) window should be closed on CursorMoved when is 1. " - highlight: (optional) highlight of window, default to 'CocFloating' " - borderhighlight: (optional) should be array or string for border highlights, " highlight all borders with first value. " - close: (optional) show close button when is 1. " - highlights: (optional) highlight items. " - buttons: (optional) array of button text for create buttons at bottom. " - codes: (optional) list of CodeBlock. " - winblend: (optional) winblend option for float window, neovim only. " - shadow: (optional) use shadow as border style, neovim only. " - focusable: (optional) neovim only, default to true. " - scrollinside: (optional) neovim only, create scrollbar inside window. " - rounded: (optional) use rounded borderchars, ignored when borderchars exists. " - zindex: (optional) zindex of window, default 50. " - borderchars: (optional) borderchars, should be length of 8 " - nopad: (optional) not add pad when 1 " - filter: (optional) filter property on vim9. " - index: (optional) line index function! coc#float#create_float_win(winid, bufnr, config) abort let lines = get(a:config, 'lines', v:null) let bufnr = a:bufnr try let bufnr = coc#float#create_buf(a:bufnr, lines, get(a:config, 'bufhidden', 'hide')) catch /E523:/ " happens when using getchar() #3921 return [] endtry " Calculate position when relative is editor if get(a:config, 'relative', '') ==# 'editor' let top = get(a:config, 'top', v:null) let bottom = get(a:config, 'bottom', v:null) let left = get(a:config, 'left', v:null) let right = get(a:config, 'right', v:null) if top isnot v:null || bottom isnot v:null || left isnot v:null || right isnot v:null let height = &lines let width = &columns " Calculate row let calc_row = a:config.row if bottom isnot v:null let calc_row = height - bottom - a:config.height - 2 elseif top isnot v:null let calc_row = top endif " Calculate col let calc_col = a:config.col if right isnot v:null let calc_col = width - right - a:config.width - 3 elseif left isnot v:null let calc_col = left endif " Check if window would overlap cursor position let pos = screenpos(0, line('.'), col('.')) let currow = pos.row - 1 let curcol = pos.col - 1 let win_top = calc_row let win_bottom = win_top + a:config.height + 2 let win_left = calc_col let win_right = win_left + a:config.width + 3 " If window would overlap cursor, switch to cursor relative if currow >= win_top && currow <= win_bottom && curcol >= win_left && curcol <= win_right let a:config.relative = 'cursor' else let a:config.row = calc_row let a:config.col = calc_col endif endif endif let lnum = max([1, get(a:config, 'index', 0) + 1]) let zindex = get(a:config, 'zindex', 50) " use exists if a:winid && coc#float#valid(a:winid) if s:is_vim let [line, col] = s:popup_position(a:config) let opts = { \ 'firstline': 1, \ 'line': line, \ 'col': col, \ 'minwidth': a:config['width'], \ 'minheight': a:config['height'], \ 'maxwidth': a:config['width'], \ 'maxheight': a:config['height'], \ 'title': get(a:config, 'title', ''), \ 'highlight': get(a:config, 'highlight', 'CocFloating'), \ 'borderhighlight': [s:get_borderhighlight(a:config)], \ } if !s:empty_border(get(a:config, 'border', [])) let opts['border'] = a:config['border'] endif call popup_setoptions(a:winid, opts) call win_execute(a:winid, 'exe '.lnum) call coc#float#vim_buttons(a:winid, a:config) call s:add_highlights(a:winid, a:config, 0) return [a:winid, winbufnr(a:winid)] else let config = s:convert_config_nvim(a:config, 0) let hlgroup = get(a:config, 'highlight', 'CocFloating') let current = getwinvar(a:winid, '&winhl', '') let winhl = coc#util#merge_winhl(current, [['Normal', hlgroup], ['FoldColumn', hlgroup]]) if winhl !=# current call setwinvar(a:winid, '&winhl', winhl) endif call nvim_win_set_buf(a:winid, bufnr) call nvim_win_set_config(a:winid, config) call nvim_win_set_cursor(a:winid, [lnum, 0]) call coc#float#nvim_create_related(a:winid, config, a:config) call s:add_highlights(a:winid, a:config, 0) return [a:winid, bufnr] endif endif let winid = 0 if s:is_vim let [line, col] = s:popup_position(a:config) let title = get(a:config, 'title', '') let buttons = get(a:config, 'buttons', []) let hlgroup = get(a:config, 'highlight', 'CocFloating') let nopad = get(a:config, 'nopad', 0) let border = s:empty_border(get(a:config, 'border', [])) ? [0, 0, 0, 0] : a:config['border'] let opts = { \ 'title': title, \ 'line': line, \ 'col': col, \ 'fixed': 1, \ 'padding': [0, !nopad && !border[1], 0, !nopad && !border[3]], \ 'borderchars': s:get_borderchars(a:config), \ 'highlight': hlgroup, \ 'minwidth': a:config['width'], \ 'minheight': a:config['height'], \ 'maxwidth': a:config['width'], \ 'maxheight': a:config['height'], \ 'close': get(a:config, 'close', 0) ? 'button' : 'none', \ 'border': border, \ 'zindex': zindex, \ 'callback': { -> coc#float#on_close(winid)}, \ 'borderhighlight': [s:get_borderhighlight(a:config)], \ 'scrollbarhighlight': 'CocFloatSbar', \ 'thumbhighlight': 'CocFloatThumb', \ } if type(get(a:config, 'filter', v:null)) == v:t_func let opts['filter'] = get(a:config, 'filter', v:null) endif noa let winid = popup_create(bufnr, opts) call s:set_float_defaults(winid, a:config) call win_execute(winid, 'exe '.lnum) call coc#float#vim_buttons(winid, a:config) else let config = s:convert_config_nvim(a:config, 1) noa let winid = nvim_open_win(bufnr, 0, config) if winid is 0 return [] endif " cursorline highlight not work on old neovim call s:set_float_defaults(winid, a:config) call nvim_win_set_cursor(winid, [lnum, 0]) call coc#float#nvim_create_related(winid, config, a:config) call coc#float#nvim_set_winblend(winid, get(a:config, 'winblend', v:null)) endif call s:add_highlights(winid, a:config, 1) let g:coc_last_float_win = winid call coc#util#do_autocmd('CocOpenFloat') return [winid, bufnr] endfunction function! coc#float#nvim_create_related(winid, config, opts) abort let related = getwinvar(a:winid, 'related', []) let exists = !empty(related) let border = get(a:opts, 'border', []) let borderhighlight = s:get_borderhighlight(a:opts) let buttons = get(a:opts, 'buttons', []) let pad = !get(a:opts, 'nopad', 0) && (empty(border) || get(border, 1, 0) == 0) let shadow = get(a:opts, 'shadow', 0) if get(a:opts, 'close', 0) call coc#float#nvim_close_btn(a:config, a:winid, border, borderhighlight, related) elseif exists call coc#float#close_related(a:winid, 'close') endif if !empty(buttons) call coc#float#nvim_buttons(a:config, a:winid, buttons, get(a:opts, 'getchar', 0), get(border, 2, 0), pad, borderhighlight, shadow, related) elseif exists call coc#float#close_related(a:winid, 'buttons') endif if !s:empty_border(border) let borderchars = s:get_borderchars(a:opts) call coc#float#nvim_border_win(a:config, borderchars, a:winid, border, get(a:opts, 'title', ''), !empty(buttons), borderhighlight, shadow, related) elseif exists call coc#float#close_related(a:winid, 'border') endif " Check right border if pad call coc#float#nvim_right_pad(a:config, a:winid, shadow, related) elseif exists call coc#float#close_related(a:winid, 'pad') endif call setwinvar(a:winid, 'related', filter(related, 'nvim_win_is_valid(v:val)')) endfunction " border window for neovim, content config with border function! coc#float#nvim_border_win(config, borderchars, winid, border, title, hasbtn, hlgroup, shadow, related) abort let winid = coc#float#get_related(a:winid, 'border') let row = a:border[0] ? a:config['row'] - 1 : a:config['row'] let col = a:border[3] ? a:config['col'] - 1 : a:config['col'] let width = a:config['width'] + a:border[1] + a:border[3] let height = a:config['height'] + a:border[0] + a:border[2] + (a:hasbtn ? 2 : 0) let lines = coc#float#create_border_lines(a:border, a:borderchars, a:title, a:config['width'], a:config['height'], a:hasbtn) let bufnr = winid ? winbufnr(winid) : 0 let bufnr = coc#float#create_buf(bufnr, lines) let opt = { \ 'relative': a:config['relative'], \ 'width': width, \ 'height': height, \ 'row': row, \ 'col': col, \ 'focusable': v:false, \ 'style': 'minimal', \ } if has_key(a:config, 'zindex') let opt['zindex'] = a:config['zindex'] endif if a:shadow && !a:hasbtn && a:border[2] let opt['border'] = 'shadow' endif if winid call nvim_win_set_config(winid, opt) call setwinvar(winid, '&winhl', 'Normal:'.a:hlgroup) else noa let winid = nvim_open_win(bufnr, 0, opt) call setwinvar(winid, 'delta', -1) let winhl = 'Normal:'.a:hlgroup call s:nvim_add_related(winid, a:winid, 'border', winhl, a:related) endif endfunction " neovim only function! coc#float#nvim_close_btn(config, winid, border, hlgroup, related) abort let winid = coc#float#get_related(a:winid, 'close') let config = { \ 'relative': a:config['relative'], \ 'width': 1, \ 'height': 1, \ 'row': get(a:border, 0, 0) ? a:config['row'] - 1 : a:config['row'], \ 'col': a:config['col'] + a:config['width'], \ 'focusable': v:true, \ 'style': 'minimal', \ } if has_key(a:config, 'zindex') let config['zindex'] = a:config['zindex'] + 2 endif if winid call nvim_win_set_config(winid, coc#dict#pick(config, ['relative', 'row', 'col'])) else let bufnr = coc#float#create_buf(0, ['X']) noa let winid = nvim_open_win(bufnr, 0, config) let winhl = 'Normal:'.a:hlgroup call setwinvar(winid, 'delta', -1) call s:nvim_add_related(winid, a:winid, 'close', winhl, a:related) endif endfunction " Create padding window by config of current window & border config function! coc#float#nvim_right_pad(config, winid, shadow, related) abort let winid = coc#float#get_related(a:winid, 'pad') let config = { \ 'relative': a:config['relative'], \ 'width': 1, \ 'height': a:config['height'], \ 'row': a:config['row'], \ 'col': a:config['col'] + a:config['width'], \ 'focusable': v:false, \ 'style': 'minimal', \ } if has_key(a:config, 'zindex') let config['zindex'] = a:config['zindex'] + 1 endif if a:shadow let config['border'] = 'shadow' endif if winid && nvim_win_is_valid(winid) call nvim_win_set_config(winid, coc#dict#pick(config, ['relative', 'row', 'col'])) call nvim_win_set_height(winid, config['height']) return endif let s:pad_bufnr = bufloaded(s:pad_bufnr) ? s:pad_bufnr : coc#float#create_buf(0, repeat([''], &lines), 'hide') noa let winid = nvim_open_win(s:pad_bufnr, 0, config) call s:nvim_add_related(winid, a:winid, 'pad', '', a:related) endfunction " draw buttons window for window with config function! coc#float#nvim_buttons(config, winid, buttons, getchar, borderbottom, pad, borderhighlight, shadow, related) abort let winid = coc#float#get_related(a:winid, 'buttons') let width = a:config['width'] + (a:pad ? 1 : 0) let config = { \ 'row': a:config['row'] + a:config['height'], \ 'col': a:config['col'], \ 'width': width, \ 'height': 2 + (a:borderbottom ? 1 : 0), \ 'relative': a:config['relative'], \ 'focusable': 1, \ 'style': 'minimal', \ 'zindex': 300, \ } if a:shadow let config['border'] = 'shadow' endif if winid let bufnr = winbufnr(winid) call s:create_btns_buffer(bufnr, width, a:buttons, a:borderbottom) call nvim_win_set_config(winid, config) else let bufnr = s:create_btns_buffer(0, width, a:buttons, a:borderbottom) noa let winid = nvim_open_win(bufnr, 0, config) if winid call s:nvim_add_related(winid, a:winid, 'buttons', '', a:related) call s:nvim_create_keymap(winid) endif endif if bufnr call nvim_buf_clear_namespace(bufnr, -1, 0, -1) call nvim_buf_add_highlight(bufnr, 1, a:borderhighlight, 0, 0, -1) if a:borderbottom call nvim_buf_add_highlight(bufnr, 1, a:borderhighlight, 2, 0, -1) endif let vcols = getbufvar(bufnr, 'vcols', []) " TODO need change vol to col for col in vcols call nvim_buf_add_highlight(bufnr, 1, a:borderhighlight, 1, col, col + 3) endfor if a:getchar let keys = s:gen_filter_keys(getbufline(bufnr, 2)[0]) call matchaddpos('MoreMsg', map(keys[0], "[2,v:val]"), 99, -1, {'window': winid}) call timer_start(10, {-> coc#float#getchar(winid, keys[1])}) endif endif endfunction function! coc#float#getchar(winid, keys) abort let ch = coc#prompt#getc() let target = getwinvar(a:winid, 'target_winid', 0) if ch ==# "\" call coc#float#close(target) return endif if ch ==# "\" if getwinvar(v:mouse_winid, 'kind', '') ==# 'close' call coc#float#close(target) return endif if v:mouse_winid == a:winid && v:mouse_lnum == 2 let vcols = getbufvar(winbufnr(a:winid), 'vcols', []) let col = v:mouse_col - 1 if index(vcols, col) < 0 let filtered = filter(vcols, 'v:val < col') call coc#rpc#notify('FloatBtnClick', [winbufnr(target), len(filtered)]) call coc#float#close(target) return endif endif else let idx = index(a:keys, ch) if idx >= 0 call coc#rpc#notify('FloatBtnClick', [winbufnr(target), idx]) call coc#float#close(target) return endif endif call coc#float#getchar(a:winid, a:keys) endfunction " Create or refresh scrollbar for winid " Need called on create, config, buffer change, scrolled function! coc#float#nvim_scrollbar(winid) abort if s:is_vim return endif let winids = nvim_tabpage_list_wins(nvim_get_current_tabpage()) if index(winids, a:winid) == -1 return endif let config = nvim_win_get_config(a:winid) let [row, column] = nvim_win_get_position(a:winid) let relative = 'editor' if row == 0 && column == 0 " fix bad value when ext_multigrid is enabled. https://github.com/neovim/neovim/issues/11935 let [row, column] = [config.row, config.col] let relative = config.relative endif let width = nvim_win_get_width(a:winid) let height = nvim_win_get_height(a:winid) let bufnr = winbufnr(a:winid) let cw = getwinvar(a:winid, '&foldcolumn', 0) ? width - 1 : width let ch = coc#float#content_height(bufnr, cw, getwinvar(a:winid, '&wrap')) let closewin = coc#float#get_related(a:winid, 'close') let border = getwinvar(a:winid, 'border', []) let scrollinside = getwinvar(a:winid, 'scrollinside', 0) && get(border, 1, 0) let winblend = getwinvar(a:winid, '&winblend', 0) let move_down = closewin && !get(border, 0, 0) let id = coc#float#get_related(a:winid, 'scrollbar') if ch <= height || height <= 1 " no scrollbar, remove exists if id call s:close_win(id, 1) endif return endif if move_down let height = height - 1 endif call coc#float#close_related(a:winid, 'pad') let sbuf = id ? winbufnr(id) : 0 let sbuf = coc#float#create_buf(sbuf, repeat([' '], height)) let opts = { \ 'row': move_down ? row + 1 : row, \ 'col': column + width - scrollinside, \ 'relative': relative, \ 'width': 1, \ 'height': height, \ 'focusable': v:false, \ 'style': 'minimal', \ } if has_key(config, 'zindex') let opts['zindex'] = config['zindex'] + 2 endif if s:has_shadow(config) let opts['border'] = 'shadow' endif if id call nvim_win_set_config(id, opts) else noa let id = nvim_open_win(sbuf, 0 , opts) if id == 0 return endif if winblend call setwinvar(id, '&winblend', winblend) endif call setwinvar(id, 'kind', 'scrollbar') call setwinvar(id, 'target_winid', a:winid) call coc#float#add_related(id, a:winid) endif if !scrollinside call coc#float#nvim_scroll_adjust(a:winid) endif " The min with height - 1 ensures that the scrollbar never takes up the full height. " If ch <= height we never reach this point, so we always want an actual scrollbar here. " The height of the scrollbar needs to be an integer to conform to the terminal grid. " Rounding down could result in gaps appearing when using coc#float#scroll(1), " meaning a situation where the lower end of the scrollbar is above a certain position, " and after calling coc#float#scroll(1) the upper end of the scrollbar is below this position, " so the position is never part of the scrollbar, " giving the appearance that some of the text in the float is skipped. " Rounding up ensures that no such gaps can appear. let thumb_height = min([height - 1, float2nr(ceil(height * (height + 0.0)/ch))]) let wininfo = getwininfo(a:winid)[0] let start = 0 if wininfo['topline'] != 1 " needed for correct getwininfo let firstline = wininfo['topline'] let lastline = s:nvim_get_botline(firstline, height, cw, bufnr) let linecount = nvim_buf_line_count(winbufnr(a:winid)) if lastline >= linecount let start = height - thumb_height else let start = max([1, float2nr(round((height - thumb_height + 0.0)*(firstline - 1.0)/(ch - height)))]) endif endif " add highlights call nvim_buf_clear_namespace(sbuf, -1, 0, -1) for idx in range(0, height - 1) if idx >= start && idx < start + thumb_height call nvim_buf_add_highlight(sbuf, -1, 'CocFloatThumb', idx, 0, 1) else call nvim_buf_add_highlight(sbuf, -1, 'CocFloatSbar', idx, 0, 1) endif endfor endfunction function! coc#float#create_border_lines(border, borderchars, title, width, height, hasbtn) abort let borderchars = a:borderchars let list = [] if a:border[0] let top = (a:border[3] ? borderchars[4]: '') \.repeat(borderchars[0], a:width) \.(a:border[1] ? borderchars[5] : '') if !empty(a:title) let top = coc#string#compose(top, 1, a:title.' ') endif call add(list, top) endif let mid = (a:border[3] ? borderchars[3]: '') \.repeat(' ', a:width) \.(a:border[1] ? borderchars[1] : '') call extend(list, repeat([mid], a:height + (a:hasbtn ? 2 : 0))) if a:hasbtn let list[len(list) - 2] = (a:border[3] ? s:borderjoinchars[3]: '') \.repeat(' ', a:width) \.(a:border[1] ? s:borderjoinchars[1] : '') endif if a:border[2] let bot = (a:border[3] ? borderchars[7]: '') \.repeat(borderchars[2], a:width) \.(a:border[1] ? borderchars[6] : '') call add(list, bot) endif return list endfunction " Close float window by id function! coc#float#close(winid, ...) abort let noautocmd = get(a:, 1, 0) if a:winid >= 0 call coc#float#close_related(a:winid) call s:close_win(a:winid, noautocmd) endif return 1 endfunction " Get visible float windows function! coc#float#get_float_win_list(...) abort let res = [] let list_all = get(a:, 1, 0) if s:is_vim return filter(popup_list(), 'popup_getpos(v:val)["visible"]'.(list_all ? '' : '&& getwinvar(v:val, "float", 0)')) else let res = [] for id in nvim_list_wins() let config = nvim_win_get_config(id) if empty(config) || empty(config['relative']) continue endif " ignore border & button window & others if list_all == 0 && !getwinvar(id, 'float', 0) continue endif call add(res, id) endfor return res endif return [] endfunction function! coc#float#get_float_by_kind(kind) abort if s:is_vim return get(filter(popup_list(), 'popup_getpos(v:val)["visible"] && getwinvar(v:val, "kind", "") ==# "'.a:kind.'"'), 0, 0) else let res = [] for winid in nvim_list_wins() let config = nvim_win_get_config(winid) if !empty(config['relative']) && getwinvar(winid, 'kind', '') ==# a:kind return winid endif endfor endif return 0 endfunction " Check if a float window is scrollable function! coc#float#scrollable(winid) abort let bufnr = winbufnr(a:winid) if bufnr == -1 return 0 endif if s:is_vim let pos = popup_getpos(a:winid) if get(pos, 'scrollbar', 0) return 1 endif let ch = coc#float#content_height(bufnr, pos['core_width'], getwinvar(a:winid, '&wrap')) return ch > pos['core_height'] else let height = nvim_win_get_height(a:winid) let width = nvim_win_get_width(a:winid) if width > 1 && getwinvar(a:winid, '&foldcolumn', 0) " since we use foldcolumn for left padding let width = width - 1 endif let ch = coc#float#content_height(bufnr, width, getwinvar(a:winid, '&wrap')) return ch > height endif endfunction function! coc#float#has_scroll() abort let win_ids = filter(coc#float#get_float_win_list(), 'coc#float#scrollable(v:val)') return !empty(win_ids) endfunction function! coc#float#scroll(forward, ...) let amount = get(a:, 1, 0) let winids = filter(coc#float#get_float_win_list(), 'coc#float#scrollable(v:val) && getwinvar(v:val,"kind","") !=# "pum"') if empty(winids) return mode() =~ '^i' || mode() ==# 'v' ? "" : "\" endif for winid in winids call s:scroll_win(winid, a:forward, amount) endfor return mode() =~ '^i' || mode() ==# 'v' ? "" : "\" endfunction function! coc#float#scroll_win(winid, forward, amount) abort let opts = coc#float#get_options(a:winid) let lines = getbufline(winbufnr(a:winid), 1, '$') let maxfirst = s:max_firstline(lines, opts['height'], opts['width']) let topline = opts['topline'] let height = opts['height'] let width = opts['width'] let scrolloff = getwinvar(a:winid, '&scrolloff', 0) if a:forward && topline >= maxfirst return endif if !a:forward && topline == 1 return endif if a:amount == 0 let topline = s:get_topline(opts['topline'], lines, a:forward, height, width) else let topline = topline + (a:forward ? a:amount : - a:amount) endif let topline = a:forward ? min([maxfirst, topline]) : max([1, topline]) let lnum = s:get_cursorline(topline, lines, scrolloff, width, height) call s:win_setview(a:winid, topline, lnum) let top = coc#float#get_options(a:winid)['topline'] " not changed if top == opts['topline'] if a:forward call s:win_setview(a:winid, topline + 1, lnum + 1) else call s:win_setview(a:winid, topline - 1, lnum - 1) endif endif endfunction function! coc#float#content_height(bufnr, width, wrap) abort if !bufloaded(a:bufnr) return 0 endif if !a:wrap return coc#compat#buf_line_count(a:bufnr) endif let lines = s:is_vim ? getbufline(a:bufnr, 1, '$') : nvim_buf_get_lines(a:bufnr, 0, -1, 0) return coc#string#content_height(lines, a:width) endfunction function! coc#float#nvim_refresh_scrollbar(winid) abort let id = coc#float#get_related(a:winid, 'scrollbar') if id && nvim_win_is_valid(id) call coc#float#nvim_scrollbar(a:winid) endif endfunction function! coc#float#on_close(winid) abort let winids = coc#float#get_float_win_list() for winid in winids let target = getwinvar(winid, 'target_winid', -1) if target == a:winid call coc#float#close(winid) endif endfor endfunction " Close related windows, or specific kind function! coc#float#close_related(winid, ...) abort if !coc#float#valid(a:winid) return endif let timer = coc#window#get_var(a:winid, 'timer', 0) if timer call timer_stop(timer) endif let kind = get(a:, 1, '') let winids = coc#window#get_var(a:winid, 'related', []) for id in winids let curr = coc#window#get_var(id, 'kind', '') if empty(kind) || curr ==# kind if curr == 'list' call coc#float#close(id, 1) elseif s:is_vim " vim doesn't throw noa call popup_close(id) else silent! noa call nvim_win_close(id, 1) endif endif endfor endfunction " Close related windows if target window is not visible. function! coc#float#check_related() abort let invalids = [] let ids = coc#float#get_float_win_list(1) for id in ids let target = getwinvar(id, 'target_winid', 0) if target && index(ids, target) == -1 call add(invalids, id) endif endfor for id in invalids call coc#float#close(id) endfor endfunction " Show float window/popup for user confirm. " Create buttons popup on vim function! coc#float#vim_buttons(winid, config) abort let related = getwinvar(a:winid, 'related', []) let winid = coc#float#get_related(a:winid, 'buttons') let btns = get(a:config, 'buttons', []) if empty(btns) if winid call s:close_win(winid, 1) " fix padding let opts = popup_getoptions(a:winid) let padding = get(opts, 'padding', v:null) if !empty(padding) let padding[2] = padding[2] - 2 endif call popup_setoptions(a:winid, {'padding': padding}) endif return endif let border = get(a:config, 'border', v:null) if !winid " adjusting popup padding let opts = popup_getoptions(a:winid) let padding = get(opts, 'padding', v:null) if type(padding) == 7 let padding = [0, 0, 2, 0] elseif len(padding) == 0 let padding = [1, 1, 3, 1] else let padding[2] = padding[2] + 2 endif call popup_setoptions(a:winid, {'padding': padding}) endif let borderhighlight = get(get(a:config, 'borderhighlight', []), 0, '') let pos = popup_getpos(a:winid) let bw = empty(border) ? 0 : get(border, 1, 0) + get(border, 3, 0) let borderbottom = empty(border) ? 0 : get(border, 2, 0) let borderleft = empty(border) ? 0 : get(border, 3, 0) let width = pos['width'] - bw + get(pos, 'scrollbar', 0) let bufnr = s:create_btns_buffer(winid ? winbufnr(winid): 0,width, btns, borderbottom) let height = 2 + (borderbottom ? 1 : 0) let keys = s:gen_filter_keys(getbufline(bufnr, 2)[0]) let options = { \ 'filter': {id, key -> coc#float#vim_filter(id, key, keys[1])}, \ 'highlight': get(opts, 'highlight', 'CocFloating') \ } let config = { \ 'line': pos['line'] + pos['height'] - height, \ 'col': pos['col'] + borderleft, \ 'minwidth': width, \ 'minheight': height, \ 'maxwidth': width, \ 'maxheight': height, \ } if winid != 0 call popup_move(winid, config) call popup_setoptions(winid, options) call win_execute(winid, 'call clearmatches()') else let options = extend({ \ 'filtermode': 'nvi', \ 'padding': [0, 0, 0, 0], \ 'fixed': 1, \ 'zindex': 99, \ }, options) call extend(options, config) let winid = popup_create(bufnr, options) endif if winid != 0 if !empty(borderhighlight) call coc#highlight#add_highlight(bufnr, -1, borderhighlight, 0, 0, -1) call coc#highlight#add_highlight(bufnr, -1, borderhighlight, 2, 0, -1) call win_execute(winid, 'call matchadd("'.borderhighlight.'", "'.s:borderchars[1].'")') endif call setwinvar(winid, 'kind', 'buttons') call setwinvar(winid, 'target_winid', a:winid) call add(related, winid) call setwinvar(a:winid, 'related', related) call matchaddpos('MoreMsg', map(keys[0], "[2,v:val]"), 99, -1, {'window': winid}) endif endfunction function! coc#float#nvim_float_click() abort let kind = getwinvar(win_getid(), 'kind', '') if kind == 'buttons' if line('.') != 2 return endif let vw = strdisplaywidth(strpart(getline('.'), 0, col('.') - 1)) let vcols = getbufvar(bufnr('%'), 'vcols', []) if index(vcols, vw) >= 0 return endif let idx = 0 if !empty(vcols) let filtered = filter(vcols, 'v:val < vw') let idx = idx + len(filtered) endif let winid = win_getid() let target = getwinvar(winid, 'target_winid', 0) if target call coc#rpc#notify('FloatBtnClick', [winbufnr(target), idx]) call coc#float#close(target) endif elseif kind == 'close' let target = getwinvar(win_getid(), 'target_winid', 0) call coc#float#close(target) endif endfunction " Add mapping if necessary function! coc#float#nvim_win_enter(winid) abort let kind = getwinvar(a:winid, 'kind', '') if kind == 'buttons' || kind == 'close' if empty(maparg('', 'n')) nnoremap :call coc#float#nvim_float_click() endif endif endfunction function! coc#float#vim_filter(winid, key, keys) abort let key = tolower(a:key) let idx = index(a:keys, key) let target = getwinvar(a:winid, 'target_winid', 0) if target && idx >= 0 call coc#rpc#notify('FloatBtnClick', [winbufnr(target), idx]) call coc#float#close(target) return 1 endif return 0 endfunction function! coc#float#get_related(winid, kind, ...) abort if coc#float#valid(a:winid) for winid in coc#window#get_var(a:winid, 'related', []) if coc#window#get_var(winid, 'kind', '') ==# a:kind return winid endif endfor endif return get(a:, 1, 0) endfunction function! coc#float#get_row(winid) abort let winid = s:is_vim ? a:winid : coc#float#get_related(a:winid, 'border', a:winid) if coc#float#valid(winid) if s:is_vim let pos = popup_getpos(winid) return pos['line'] - 1 endif let pos = nvim_win_get_position(winid) return pos[0] endif endfunction " Create temporarily buffer with optional lines and &bufhidden function! coc#float#create_buf(bufnr, ...) abort if a:bufnr > 0 && bufloaded(a:bufnr) let bufnr = a:bufnr else if s:is_vim noa let bufnr = bufadd('') noa call bufload(bufnr) call setbufvar(bufnr, '&buflisted', 0) call setbufvar(bufnr, '&modeline', 0) call setbufvar(bufnr, '&buftype', 'nofile') call setbufvar(bufnr, '&swapfile', 0) else noa let bufnr = nvim_create_buf(v:false, v:true) endif let bufhidden = get(a:, 2, 'wipe') call setbufvar(bufnr, '&bufhidden', bufhidden) call setbufvar(bufnr, '&undolevels', -1) " neovim's bug call setbufvar(bufnr, '&modifiable', 1) endif let lines = get(a:, 1, v:null) if type(lines) == v:t_list if s:is_vim silent noa call setbufline(bufnr, 1, lines) silent noa call deletebufline(bufnr, len(lines) + 1, '$') else call nvim_buf_set_lines(bufnr, 0, -1, v:false, lines) endif endif return bufnr endfunction " Change border window & close window when scrollbar is shown. function! coc#float#nvim_scroll_adjust(winid) abort let winid = coc#float#get_related(a:winid, 'border') if !winid return endif let bufnr = winbufnr(winid) let lines = nvim_buf_get_lines(bufnr, 0, -1, 0) if len(lines) >= 2 let cw = nvim_win_get_width(a:winid) let width = nvim_win_get_width(winid) if width - cw != 1 + (strcharpart(lines[1], 0, 1) ==# s:borderchars[3] ? 1 : 0) return endif call nvim_win_set_width(winid, width + 1) let lastline = len(lines) - 1 for i in range(0, lastline) let line = lines[i] if i == 0 let add = s:borderchars[0] elseif i == lastline let add = s:borderchars[2] else let add = ' ' endif let prev = strcharpart(line, 0, strchars(line) - 1) let lines[i] = prev . add . coc#string#last_character(line) endfor call nvim_buf_set_lines(bufnr, 0, -1, 0, lines) " Move right close button if coc#window#get_var(a:winid, 'right', 0) == 0 let id = coc#float#get_related(a:winid, 'close') if id let [row, col] = nvim_win_get_position(id) call nvim_win_set_config(id, { \ 'relative': 'editor', \ 'row': row, \ 'col': col + 1, \ }) endif else " Move winid and all related left by 1 let winids = [a:winid] + coc#window#get_var(a:winid, 'related', []) for winid in winids if nvim_win_is_valid(winid) if coc#window#get_var(winid, 'kind', '') != 'close' let config = nvim_win_get_config(winid) let [row, column] = [config.row, config.col] call nvim_win_set_config(winid, { \ 'row': row, \ 'col': column - 1, \ 'relative': 'editor', \ }) endif endif endfor endif endif endfunction function! coc#float#nvim_set_winblend(winid, winblend) abort if a:winblend is v:null return endif call coc#window#set_var(a:winid, '&winblend', a:winblend) for winid in coc#window#get_var(a:winid, 'related', []) call coc#window#set_var(winid, '&winblend', a:winblend) endfor endfunction function! s:popup_visible(id) abort let pos = popup_getpos(a:id) if !empty(pos) && get(pos, 'visible', 0) return 1 endif return 0 endfunction function! s:convert_config_nvim(config, create) abort let valids = ['relative', 'win', 'anchor', 'width', 'height', 'bufpos', 'col', 'row', 'focusable'] let result = coc#dict#pick(a:config, valids) let border = get(a:config, 'border', []) if !s:empty_border(border) if result['relative'] ==# 'cursor' && result['row'] < 0 " move top when has bottom border if get(border, 2, 0) let result['row'] = result['row'] - 1 endif else " move down when has top border if get(border, 0, 0) && !get(a:config, 'prompt', 0) let result['row'] = result['row'] + 1 endif endif " move right when has left border if get(border, 3, 0) let result['col'] = result['col'] + 1 endif let result['width'] = float2nr(result['width'] + 1 - get(border,3, 0)) else let result['width'] = float2nr(result['width'] + (get(a:config, 'nopad', 0) ? 0 : 1)) endif if get(a:config, 'shadow', 0) && a:create if empty(get(a:config, 'buttons', v:null)) && empty(get(border, 2, 0)) let result['border'] = 'shadow' endif endif let result['zindex'] = get(a:config, 'zindex', 50) let result['height'] = float2nr(result['height']) return result endfunction function! s:create_btns_buffer(bufnr, width, buttons, borderbottom) abort let n = len(a:buttons) let spaces = a:width - n + 1 let tw = 0 for txt in a:buttons let tw += strdisplaywidth(txt) endfor if spaces < tw throw 'window is too small for buttons.' endif let ds = (spaces - tw)/n let dl = ds/2 let dr = ds%2 == 0 ? ds/2 : ds/2 + 1 let btnline = '' let idxes = [] for idx in range(0, n - 1) let txt = toupper(a:buttons[idx][0]).a:buttons[idx][1:] let btnline .= repeat(' ', dl).txt.repeat(' ', dr) if idx != n - 1 call add(idxes, strdisplaywidth(btnline)) let btnline .= s:borderchars[1] endif endfor let lines = [repeat(s:borderchars[0], a:width), btnline] if a:borderbottom call add(lines, repeat(s:borderchars[0], a:width)) endif for idx in idxes let lines[0] = strcharpart(lines[0], 0, idx).s:borderjoinchars[0].strcharpart(lines[0], idx + 1) if a:borderbottom let lines[2] = strcharpart(lines[0], 0, idx).s:borderjoinchars[2].strcharpart(lines[0], idx + 1) endif endfor let bufnr = coc#float#create_buf(a:bufnr, lines) call setbufvar(bufnr, 'vcols', idxes) return bufnr endfunction function! s:gen_filter_keys(line) abort let cols = [] let used = [] let next = 1 for idx in range(0, strchars(a:line) - 1) let ch = strcharpart(a:line, idx, 1) let nr = char2nr(ch) if next if (nr >= 65 && nr <= 90) || (nr >= 97 && nr <= 122) let lc = tolower(ch) if index(used, lc) < 0 && (!s:is_vim || empty(maparg(lc, 'n'))) let col = len(strcharpart(a:line, 0, idx)) + 1 call add(used, lc) call add(cols, col) let next = 0 endif endif else if ch == s:borderchars[1] let next = 1 endif endif endfor return [cols, used] endfunction function! s:close_win(winid, noautocmd) abort if a:winid <= 0 return endif " vim not throw for none exists winid if s:is_vim let prefix = a:noautocmd ? 'noa ': '' exe prefix.'call popup_close('.a:winid.')' else if nvim_win_is_valid(a:winid) let prefix = a:noautocmd ? 'noa ': '' exe prefix.'call nvim_win_close('.a:winid.', 1)' endif endif endfunction function! s:nvim_create_keymap(winid) abort let bufnr = winbufnr(a:winid) call nvim_buf_set_keymap(bufnr, 'n', '', ':call coc#float#nvim_float_click()', { \ 'silent': v:true, \ 'nowait': v:true \ }) endfunction " getwininfo is buggy on neovim, use topline, width & height should for content function! s:nvim_get_botline(topline, height, width, bufnr) abort let lines = getbufline(a:bufnr, a:topline, a:topline + a:height - 1) let botline = a:topline let count = 0 for i in range(0, len(lines) - 1) let w = max([1, strdisplaywidth(lines[i])]) let lh = float2nr(ceil(str2float(string(w))/a:width)) let count = count + lh let botline = a:topline + i if count >= a:height break endif endfor return botline endfunction " get popup position for vim8 based on config of neovim float window function! s:popup_position(config) abort let relative = get(a:config, 'relative', 'editor') let border = get(a:config, 'border', [0, 0, 0, 0]) let delta = get(border, 0, 0) + get(border, 2, 0) if relative ==# 'cursor' if a:config['row'] < 0 let delta = - delta elseif a:config['row'] == 0 let delta = - get(border, 0, 0) else let delta = 0 endif return [s:popup_cursor(a:config['row'] + delta), s:popup_cursor(a:config['col'])] endif return [a:config['row'] + 1, a:config['col'] + 1] endfunction function! coc#float#add_related(winid, target) abort let arr = coc#window#get_var(a:target, 'related', []) if index(arr, a:winid) >= 0 return endif call add(arr, a:winid) call coc#window#set_var(a:target, 'related', arr) endfunction function! coc#float#get_wininfo(winid) abort if !coc#float#valid(a:winid) throw 'Not valid float window: '.a:winid endif if s:is_vim let pos = popup_getpos(a:winid) return {'topline': pos['firstline'], 'botline': pos['lastline']} endif let info = getwininfo(a:winid)[0] return {'topline': info['topline'], 'botline': info['botline']} endfunction function! s:popup_cursor(n) abort if a:n == 0 return 'cursor' endif if a:n < 0 return 'cursor'.a:n endif return 'cursor+'.a:n endfunction " max firstline of lines, height > 0, width > 0 function! s:max_firstline(lines, height, width) abort let max = len(a:lines) let remain = a:height for line in reverse(copy(a:lines)) let w = max([1, strdisplaywidth(line)]) let dh = float2nr(ceil(str2float(string(w))/a:width)) if remain - dh < 0 break endif let remain = remain - dh let max = max - 1 endfor return min([len(a:lines), max + 1]) endfunction " Get best lnum by topline function! s:get_cursorline(topline, lines, scrolloff, width, height) abort let lastline = len(a:lines) if a:topline == lastline return lastline endif let bottomline = a:topline let used = 0 for lnum in range(a:topline, lastline) let w = max([1, strdisplaywidth(a:lines[lnum - 1])]) let dh = float2nr(ceil(str2float(string(w))/a:width)) if used + dh >= a:height || lnum == lastline let bottomline = lnum break endif let used += dh endfor let cursorline = a:topline + a:scrolloff if cursorline + a:scrolloff > bottomline " unable to satisfy scrolloff let cursorline = (a:topline + bottomline)/2 endif return cursorline endfunction " Get firstline for full scroll function! s:get_topline(topline, lines, forward, height, width) abort let used = 0 let lnums = a:forward ? range(a:topline, len(a:lines)) : reverse(range(1, a:topline)) let topline = a:forward ? len(a:lines) : 1 for lnum in lnums let w = max([1, strdisplaywidth(a:lines[lnum - 1])]) let dh = float2nr(ceil(str2float(string(w))/a:width)) if used + dh >= a:height let topline = lnum break endif let used += dh endfor if topline == a:topline if a:forward let topline = min([len(a:lines), topline + 1]) else let topline = max([1, topline - 1]) endif endif return topline endfunction " topline content_height content_width function! coc#float#get_options(winid) abort if s:is_vim let pos = popup_getpos(a:winid) return { \ 'topline': pos['firstline'], \ 'width': pos['core_width'], \ 'height': pos['core_height'] \ } else let width = nvim_win_get_width(a:winid) if coc#window#get_var(a:winid, '&foldcolumn', 0) let width = width - 1 endif let info = getwininfo(a:winid)[0] return { \ 'topline': info['topline'], \ 'height': nvim_win_get_height(a:winid), \ 'width': width \ } endif endfunction function! s:win_setview(winid, topline, lnum) abort if s:is_vim call win_execute(a:winid, 'exe '.a:lnum) call popup_setoptions(a:winid, { 'firstline': a:topline }) else call win_execute(a:winid, 'call winrestview({"lnum":'.a:lnum.',"topline":'.a:topline.'})') call timer_start(1, { -> coc#float#nvim_refresh_scrollbar(a:winid) }) endif endfunction function! s:set_float_defaults(winid, config) abort if !s:is_vim let hlgroup = get(a:config, 'highlight', 'CocFloating') call setwinvar(a:winid, '&winhl', 'Normal:'.hlgroup.',FoldColumn:'.hlgroup) call setwinvar(a:winid, 'border', get(a:config, 'border', [])) call setwinvar(a:winid, 'scrollinside', get(a:config, 'scrollinside', 0)) call setwinvar(a:winid, '&foldcolumn', s:nvim_get_foldcolumn(a:config)) call setwinvar(a:winid, '&signcolumn', 'no') call setwinvar(a:winid, '&cursorcolumn', 0) else call setwinvar(a:winid, '&foldcolumn', 0) endif if exists('&statuscolumn') call setwinvar(a:winid, '&statuscolumn', '') endif if !s:is_vim call setwinvar(a:winid, '&number', 0) call setwinvar(a:winid, '&relativenumber', 0) call setwinvar(a:winid, '&cursorline', 0) endif call setwinvar(a:winid, '&foldenable', 0) call setwinvar(a:winid, '&colorcolumn', '') call setwinvar(a:winid, '&spell', 0) call setwinvar(a:winid, '&linebreak', 1) call setwinvar(a:winid, '&conceallevel', 0) call setwinvar(a:winid, '&list', 0) call setwinvar(a:winid, '&wrap', !get(a:config, 'cursorline', 0)) call setwinvar(a:winid, '&scrolloff', 0) call setwinvar(a:winid, '&showbreak', 'NONE') call win_execute(a:winid, 'setl fillchars+=eob:\ ') if get(a:config, 'autohide', 0) call setwinvar(a:winid, 'autohide', 1) endif call setwinvar(a:winid, 'float', 1) endfunction function! s:nvim_add_related(winid, target, kind, winhl, related) abort if a:winid <= 0 return endif if exists('&statuscolumn') call setwinvar(a:winid, '&statuscolumn', '') endif let winhl = empty(a:winhl) ? coc#window#get_var(a:target, '&winhl', '') : a:winhl call setwinvar(a:winid, '&winhl', winhl) call setwinvar(a:winid, 'target_winid', a:target) call setwinvar(a:winid, 'kind', a:kind) call add(a:related, a:winid) endfunction function! s:nvim_get_foldcolumn(config) abort let nopad = get(a:config, 'nopad', 0) if nopad return 0 endif let border = get(a:config, 'border', v:null) if border is 1 || (type(border) == v:t_list && get(border, 3, 0) == 1) return 0 endif return 1 endfunction function! s:add_highlights(winid, config, create) abort let codes = get(a:config, 'codes', []) let highlights = get(a:config, 'highlights', []) if empty(codes) && empty(highlights) && a:create return endif let bgGroup = get(a:config, 'highlight', 'CocFloating') for obj in codes let hlGroup = get(obj, 'hlGroup', v:null) if !empty(hlGroup) let obj['hlGroup'] = coc#hlgroup#compose_hlgroup(hlGroup, bgGroup) endif endfor call coc#highlight#add_highlights(a:winid, codes, highlights) endfunction function! s:empty_border(border) abort if empty(a:border) || empty(filter(copy(a:border), 'v:val != 0')) return 1 endif return 0 endfunction function! s:get_borderchars(config) abort let borderchars = get(a:config, 'borderchars', []) if !empty(borderchars) return borderchars endif return get(a:config, 'rounded', 0) ? s:rounded_borderchars : s:borderchars endfunction function! s:scroll_win(winid, forward, amount) abort if s:is_vim call coc#float#scroll_win(a:winid, a:forward, a:amount) else call timer_start(0, { -> coc#float#scroll_win(a:winid, a:forward, a:amount)}) endif endfunction function! s:get_borderhighlight(config) abort let hlgroup = get(a:config, 'highlight', 'CocFloating') let borderhighlight = get(a:config, 'borderhighlight', 'CocFloatBorder') let highlight = type(borderhighlight) == 3 ? borderhighlight[0] : borderhighlight return coc#hlgroup#compose_hlgroup(highlight, hlgroup) endfunction function! s:has_shadow(config) abort let border = get(a:config, 'border', []) let filtered = filter(copy(border), 'type(v:val) == 3 && get(v:val, 1, "") ==# "FloatShadow"') return len(filtered) > 0 endfunction ================================================ FILE: autoload/coc/highlight.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:func_map = { \ 'del_markers': 'coc#vim9#Del_markers', \ 'buffer_update': 'coc#vim9#Buffer_update', \ 'update_highlights': 'coc#vim9#Update_highlights', \ 'set_highlights': 'coc#vim9#Set_highlights', \ 'clear_highlights': 'coc#vim9#Clear_highlights', \ 'add_highlight': 'coc#vim9#Add_highlight', \ 'clear_all': 'coc#vim9#Clear_all', \ 'highlight_ranges': 'coc#vim9#Highlight_ranges', \ } function! s:call(name, args) abort try if s:is_vim call call(s:func_map[a:name], a:args) else call call(v:lua.require('coc.highlight')[a:name], a:args) endif catch /.*/ " Need try catch here on vim9 let name = s:is_vim ? get(s:func_map, a:name, a:name) : 'coc.highlight.' . a:name call coc#compat#send_error(name, s:is_vim) endtry endfunction " Update highlights of whole buffer, bufnr: number, key: any, highlights: list, priority: number = 10, changedtick: any = null function! coc#highlight#buffer_update(...) abort call s:call('buffer_update', a:000) endfunction " Update highlights, id: number, key: string, highlights: list, start: number = 0, end: number = -1, priority: any = null function! coc#highlight#update_highlights(...) abort call s:call('update_highlights', a:000) endfunction " Add multiple highlights, bufnr: number, key: any, highlights: list, priority: number = 0 function! coc#highlight#set(...) abort call s:call('set_highlights', a:000) endfunction " bufnr: number, ids: list function! coc#highlight#del_markers(...) abort call s:call('del_markers', a:000) endfunction " id: number, key: any, start_line: number = 0, end_line: number = -1 function! coc#highlight#clear_highlight(...) abort call s:call('clear_highlights', a:000) endfunction " Add single highlight, id: number, src_id: number, hl_group: string, line: number, col_start: number, col_end: number, opts: dict = {} function! coc#highlight#add_highlight(...) abort call s:call('add_highlight', a:000) endfunction " Clear all extmark or textprop highlights of coc.nvim function! coc#highlight#clear_all() abort call s:call('clear_all', []) endfunction " highlight LSP ranges. id: number, key: any, hlGroup: string, ranges: list, opts: dict = {} function! coc#highlight#ranges(...) abort call s:call('highlight_ranges', a:000) endfunction " Get list of highlights, bufnr, key, [start, end] 0 based line index. function! coc#highlight#get_highlights(bufnr, key, ...) abort if s:is_vim return coc#vim9#Get_highlights(a:bufnr, a:key, get(a:, 1, 0), get(a:, 2, -1)) endif return v:lua.require('coc.highlight').get_highlights(a:bufnr, a:key, get(a:, 1, 0), get(a:, 2, -1)) endfunction " highlight buffer in winid with CodeBlock and HighlightItems " export interface HighlightItem { " lnum: number // 0 based " hlGroup: string " colStart: number // 0 based " colEnd: number " } " export interface CodeBlock { " filetype?: string " hlGroup?: string " startLine: number // 0 based " endLine: number " } function! coc#highlight#add_highlights(winid, codes, highlights) abort " clear highlights let bufnr = winbufnr(a:winid) let kind = getwinvar(a:winid, 'kind', '') if kind !=# 'pum' call win_execute(a:winid, 'syntax clear') if !empty(a:codes) call coc#highlight#highlight_lines(a:winid, a:codes) endif endif call coc#highlight#buffer_update(bufnr, -1, a:highlights) endfunction " Add highlights to line groups of winid, support hlGroup and filetype " config should have startLine, endLine (0 based, end excluded) and filetype or hlGroup " endLine should > startLine and endLine is excluded function! coc#highlight#highlight_lines(winid, blocks) abort let region_id = 1 let defined = [] let cmds = [] for config in a:blocks let start = config['startLine'] + 1 let end = config['endLine'] == -1 ? len(getbufline(winbufnr(a:winid), 1, '$')) + 1 : config['endLine'] + 1 let filetype = get(config, 'filetype', '') let hlGroup = get(config, 'hlGroup', '') if !empty(hlGroup) call add(cmds, 'syntax region '.hlGroup.' start=/\%'.start.'l/ end=/\%'.end.'l/') else let filetype = matchstr(filetype, '\v^\w+') if empty(filetype) || filetype == 'txt' || index(get(g:, 'coc_markdown_disabled_languages', []), filetype) != -1 continue endif if index(defined, filetype) == -1 call add(cmds, 'syntax include @'.toupper(filetype).' syntax/'.filetype.'.vim') call add(cmds, 'unlet! b:current_syntax') call add(defined, filetype) endif call add(cmds, 'syntax region CodeBlock'.region_id.' start=/\%'.start.'l/ end=/\%'.end.'l/ contains=@'.toupper(filetype).' keepend') let region_id = region_id + 1 endif endfor if !empty(cmds) call win_execute(a:winid, cmds, 'silent!') endif endfunction " add matches for window. winid, bufnr, ranges, hlGroup, priority function! coc#highlight#match_ranges(winid, bufnr, ranges, hlGroup, priority) abort if s:is_vim return coc#vim9#Match_ranges(a:winid, a:bufnr, a:ranges, a:hlGroup, a:priority) endif return v:lua.require('coc.highlight').match_ranges(a:winid, a:bufnr, a:ranges, a:hlGroup, a:priority) endfunction " Clear matches by hlGroup regexp, used by extension function! coc#highlight#clear_match_group(winid, match) abort call coc#window#clear_match_group(a:winid, a:match) endfunction " Clear matches by match ids, use 0 for current win. function! coc#highlight#clear_matches(winid, ids) call coc#window#clear_matches(a:winid, a:ids) endfunction function! coc#highlight#create_namespace(key) abort if type(a:key) == v:t_number return a:key endif return coc#compat#call('create_namespace', ['coc-'. a:key]) endfunction ================================================ FILE: autoload/coc/hlgroup.vim ================================================ scriptencoding utf-8 function! coc#hlgroup#valid(hlGroup) abort return hlexists(a:hlGroup) && execute('hi '.a:hlGroup, 'silent!') !~# ' cleared$' endfunction function! coc#hlgroup#compose(fg, bg) abort let fgId = synIDtrans(hlID(a:fg)) let bgId = synIDtrans(hlID(a:bg)) let isGuiReversed = synIDattr(fgId, 'reverse', 'gui') !=# '1' || synIDattr(bgId, 'reverse', 'gui') !=# '1' let guifg = isGuiReversed ? synIDattr(fgId, 'fg', 'gui') : synIDattr(fgId, 'bg', 'gui') let guibg = isGuiReversed ? synIDattr(bgId, 'bg', 'gui') : synIDattr(bgId, 'fg', 'gui') let isCtermReversed = synIDattr(fgId, 'reverse', 'cterm') !=# '1' || synIDattr(bgId, 'reverse', 'cterm') !=# '1' let ctermfg = isCtermReversed ? synIDattr(fgId, 'fg', 'cterm') : synIDattr(fgId, 'bg', 'cterm') let ctermbg = isCtermReversed ? synIDattr(bgId, 'bg', 'cterm') : synIDattr(bgId, 'fg', 'cterm') let bold = synIDattr(fgId, 'bold') ==# '1' let italic = synIDattr(fgId, 'italic') ==# '1' let underline = synIDattr(fgId, 'underline') ==# '1' let cmd = '' if !empty(guifg) let cmd .= ' guifg=' . guifg endif if !empty(ctermfg) let cmd .= ' ctermfg=' . ctermfg endif if !empty(guibg) let cmd .= ' guibg=' . guibg endif if !empty(ctermbg) let cmd .= ' ctermbg=' . ctermbg endif if bold let cmd .= ' cterm=bold gui=bold' elseif italic let cmd .= ' cterm=italic gui=italic' elseif underline let cmd .= ' cterm=underline gui=underline' endif return cmd endfunction " Compose hlGroups with foreground and background colors. function! coc#hlgroup#compose_hlgroup(fgGroup, bgGroup) abort let hlGroup = 'Fg'.a:fgGroup.'Bg'.a:bgGroup if a:fgGroup ==# a:bgGroup return a:fgGroup endif if coc#hlgroup#valid(hlGroup) return hlGroup endif let cmd = coc#hlgroup#compose(a:fgGroup, a:bgGroup) if empty(cmd) return 'Normal' endif execute 'silent hi ' . hlGroup . cmd return hlGroup endfunction " hlGroup id, key => 'fg' | 'bg', kind => 'cterm' | 'gui' function! coc#hlgroup#get_color(id, key, kind) abort if synIDattr(a:id, 'reverse', a:kind) !=# '1' return synIDattr(a:id, a:key, a:kind) endif return synIDattr(a:id, a:key ==# 'bg' ? 'fg' : 'bg', a:kind) endfunction function! coc#hlgroup#get_hl_command(id, key, cterm, gui) abort let cterm = coc#hlgroup#get_color(a:id, a:key, 'cterm') let gui = coc#hlgroup#get_color(a:id, a:key, 'gui') let cmd = ' cterm'.a:key.'=' . (empty(cterm) ? a:cterm : cterm) let cmd .= ' gui'.a:key.'=' . (empty(gui) ? a:gui : gui) return cmd endfunction function! coc#hlgroup#get_hex_color(id, kind, fallback) abort let term_colors = s:use_term_colors() let attr = coc#hlgroup#get_color(a:id, a:kind, term_colors ? 'cterm' : 'gui') let hex = s:to_hex_color(attr, term_colors) if empty(hex) && !term_colors let attr = coc#hlgroup#get_color(a:id, a:kind, 'cterm') let hex = s:to_hex_color(attr, 1) endif return empty(hex) ? a:fallback : hex endfunction function! coc#hlgroup#get_contrast(group1, group2) abort let normal = coc#hlgroup#get_hex_color(synIDtrans(hlID('Normal')), 'bg', '#000000') let bg1 = coc#hlgroup#get_hex_color(synIDtrans(hlID(a:group1)), 'bg', normal) let bg2 = coc#hlgroup#get_hex_color(synIDtrans(hlID(a:group2)), 'bg', normal) return coc#color#hex_contrast(bg1, bg2) endfunction " Darken or lighten background function! coc#hlgroup#create_bg_command(group, amount) abort let id = synIDtrans(hlID(a:group)) let normal = coc#hlgroup#get_hex_color(synIDtrans(hlID('Normal')), 'bg', &background ==# 'dark' ? '#282828' : '#fefefe') let bg = coc#hlgroup#get_hex_color(id, 'bg', normal) let hex = a:amount > 0 ? coc#color#darken(bg, a:amount) : coc#color#lighten(bg, -a:amount) let ctermbg = coc#color#rgb2term(strpart(hex, 1)) if s:use_term_colors() && !s:check_ctermbg(id, ctermbg) && abs(a:amount) < 20.0 return coc#hlgroup#create_bg_command(a:group, a:amount * 2) endif return 'ctermbg=' . ctermbg.' guibg=' . hex endfunction function! s:check_ctermbg(id, cterm) abort let attr = coc#hlgroup#get_color(a:id, 'bg', 'cterm') if empty(attr) let attr = coc#hlgroup#get_color(synIDtrans(hlID('Normal')), 'bg', 'cterm') endif if attr ==# a:cterm return 0 endif return 1 endfunction function! s:to_hex_color(color, term) abort if empty(a:color) return '' endif if a:color =~# '^#\x\+$' return a:color endif if a:term && a:color =~# '^\d\+$' return coc#color#term2rgb(a:color) endif let hex = coc#color#nameToHex(tolower(a:color), a:term) return empty(hex) ? '' : hex endfunction " Can't use script variable as nvim change it after VimEnter function! s:use_term_colors() abort return &termguicolors == 0 && !has('gui_running') endfunction ================================================ FILE: autoload/coc/inline.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:inline_ns = coc#highlight#create_namespace('inlineSuggest') let s:is_supported = has('patch-9.0.0185') || has('nvim-0.7') let s:hl_group = 'CocInlineVirtualText' let s:annot_hlgroup = 'CocInlineAnnotation' function! coc#inline#visible() abort return coc#vtext#exists(bufnr('%'), s:inline_ns) endfunction function! coc#inline#trigger(...) abort call coc#inline#clear() call CocActionAsync('inlineTrigger', bufnr('%'), get(a:, 1)) return '' endfunction function! coc#inline#cancel() abort call coc#inline#clear() call CocActionAsync('inlineCancel') return '' endfunction function! coc#inline#accept(...) abort if coc#inline#visible() call CocActionAsync('inlineAccept', bufnr('%'), get(a:, 1, 'all')) endif return '' endfunction function! coc#inline#next() abort if coc#inline#visible() call CocActionAsync('inlineNext', bufnr('%')) endif return '' endfunction function! coc#inline#prev() abort if coc#inline#visible() call CocActionAsync('inlinePrev', bufnr('%')) endif return '' endfunction function! coc#inline#clear(...) abort let bufnr = get(a:, 1, bufnr('%')) call coc#compat#call('buf_clear_namespace', [bufnr, s:inline_ns, 0, -1]) endfunction function! coc#inline#_insert(bufnr, lineidx, col, lines, annot) abort if !s:is_supported || bufnr('%') != a:bufnr || mode() !~ '^i' || col('.') != a:col return v:false endif call coc#inline#clear(a:bufnr) call coc#pum#clear_vtext() if empty(get(a:lines, 0, '')) return v:false endif let option = { \ 'col': a:col, \ 'hl_mode': 'replace', \ } let blocks = [[a:lines[0], s:hl_group]] if !empty(a:annot) let blocks += [[' '], [a:annot, s:annot_hlgroup]] endif if len(a:lines) > 1 let option['virt_lines'] = map(a:lines[1:], {idx, line -> [[line, s:hl_group]]}) endif call coc#vtext#add(a:bufnr, s:inline_ns, a:lineidx, blocks, option) return v:true endfunction ================================================ FILE: autoload/coc/list.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:prefix = '[List Preview]' let s:sign_group = 'CocList' " filetype detect could be slow. let s:filetype_map = { \ 'c': 'c', \ 'py': 'python', \ 'vim': 'vim', \ 'ts': 'typescript', \ 'js': 'javascript', \ 'html': 'html', \ 'css': 'css' \ } let s:pwinid = -1 let s:pbufnr = -1 let s:sign_range = 'CocCursorLine' let s:sign_popup_range = 'PopUpCocList' let s:current_line_hl = 'CocListCurrent' function! coc#list#getchar() abort return coc#prompt#getchar() endfunction function! coc#list#setlines(bufnr, lines, append) if a:append silent call appendbufline(a:bufnr, '$', a:lines) else if exists('*deletebufline') silent call deletebufline(a:bufnr, len(a:lines) + 1, '$') else let n = len(a:lines) + 1 let saved_reg = @" silent execute n.',$d' let @" = saved_reg endif silent call setbufline(a:bufnr, 1, a:lines) endif endfunction function! coc#list#options(...) let list = ['--top', '--tab', '--buffer', '--workspace-folder', '--normal', '--no-sort', \ '--input=', '--strict', '--regex', '--interactive', '--number-select', \ '--auto-preview', '--ignore-case', '--no-quit', '--first', '--reverse', '--height='] if get(g:, 'coc_enabled', 0) let names = coc#rpc#request('listNames', []) call extend(list, names) endif return join(list, "\n") endfunction function! coc#list#names(...) abort let names = coc#rpc#request('listNames', []) return join(names, "\n") endfunction function! coc#list#status(name) if !exists('b:list_status') | return '' | endif return get(b:list_status, a:name, '') endfunction function! coc#list#create(position, height, name, numberSelect) if a:position ==# 'tab' call coc#ui#safe_open('silent tabe', 'list:///'.a:name) else call s:save_views(-1) let height = max([1, a:height]) let cmd = 'silent keepalt '.(a:position ==# 'top' ? '' : 'botright').height.'sp' call coc#ui#safe_open(cmd, 'list:///'.a:name) call s:set_height(height) call s:restore_views() endif if a:numberSelect setl norelativenumber setl number else setl nonumber setl norelativenumber endif if exists('&winfixbuf') setl winfixbuf endif setl colorcolumn="" return [bufnr('%'), win_getid(), tabpagenr()] endfunction " close list windows function! coc#list#clean_up() abort for i in range(1, winnr('$')) let bufname = bufname(winbufnr(i)) if bufname =~# 'list://' execute i.'close!' endif endfor endfunction function! coc#list#setup(source) let b:list_status = {} setl buftype=nofile nobuflisted nofen nowrap setl norelativenumber bufhidden=wipe nocursorline winfixheight setl tabstop=1 nolist nocursorcolumn undolevels=-1 setl signcolumn=auto setl foldcolumn=0 if exists('&cursorlineopt') setl cursorlineopt=both endif if s:is_vim setl nocursorline else setl cursorline setl winhighlight=CursorLine:CocListLine endif setl scrolloff=0 setl filetype=list syntax case ignore let source = a:source[8:] let name = toupper(source[0]).source[1:] execute 'syntax match Coc'.name.'Line /\v^.*$/' if !s:is_vim " Repeat press and would invoke on vim nnoremap c endif endfunction function! coc#list#close(winid, position, target_win, saved_height) abort let tabnr = coc#window#tabnr(a:winid) if a:position ==# 'tab' if tabnr != -1 call coc#list#close_preview(tabnr, 0) endif call coc#window#close(a:winid) else call s:save_views(a:winid) if tabnr != -1 call coc#list#close_preview(tabnr, 0) endif if type(a:target_win) == v:t_number call win_gotoid(a:target_win) endif call coc#window#close(a:winid) call s:restore_views() if type(a:saved_height) == v:t_number call coc#window#set_height(a:target_win, a:saved_height) endif " call coc#rpc#notify('Log', ["close", a:target_win, v]) endif endfunction function! coc#list#select(bufnr, line) abort if s:is_vim && !empty(a:bufnr) && bufloaded(a:bufnr) call sign_unplace(s:sign_group, { 'buffer': a:bufnr }) if a:line > 0 call sign_place(6, s:sign_group, s:current_line_hl, a:bufnr, {'lnum': a:line}) endif endif endfunction " Check if previewwindow exists on current tab. function! coc#list#has_preview() if s:pwinid != -1 && coc#window#visible(s:pwinid) return 1 endif for i in range(1, winnr('$')) let preview = getwinvar(i, 'previewwindow', getwinvar(i, '&previewwindow', 0)) if preview return i endif endfor return 0 endfunction " Get previewwindow from tabnr, use 0 for current tab function! coc#list#get_preview(...) abort if s:pwinid != -1 && coc#window#visible(s:pwinid) return s:pwinid endif let tabnr = get(a:, 1, 0) == 0 ? tabpagenr() : a:1 let info = gettabinfo(tabnr) if !empty(info) for win in info[0]['windows'] if gettabwinvar(tabnr, win, 'previewwindow', 0) return win endif endfor endif return -1 endfunction function! coc#list#scroll_preview(dir, floatPreview) abort let winid = coc#list#get_preview() if winid == -1 return endif if a:floatPreview let forward = a:dir ==# 'up' ? 0 : 1 let amount = 1 if s:is_vim call coc#float#scroll_win(winid, forward, amount) else call timer_start(0, { -> coc#float#scroll_win(winid, forward, amount)}) endif return endif call win_execute(winid, "normal! ".(a:dir ==# 'up' ? "\" : "\")) endfunction function! coc#list#close_preview(...) abort let tabnr = get(a:, 1, tabpagenr()) let winid = coc#list#get_preview(tabnr) if winid != -1 let keep = get(a:, 2, 1) && tabnr == tabpagenr() && !coc#window#is_float(winid) if keep call s:save_views(winid) endif call coc#window#close(winid) if keep call s:restore_views() endif endif endfunction function! s:get_preview_lines(lines, config) abort if empty(a:lines) if get(a:config, 'scheme', 'file') !=# 'file' let bufnr = s:load_buffer(get(a:config, 'name', '')) return bufnr == 0 ? [''] : getbufline(bufnr, 1, '$') else return [''] endif endif return a:lines endfunction function! coc#list#float_preview(lines, config) abort let position = get(a:config, 'position', 'bottom') if position ==# 'tab' throw 'unable to use float preview' endif let remain = 0 let winrow = win_screenpos(winnr())[0] if position ==# 'bottom' let remain = winrow - 2 else let winbottom = winrow + winheight(winnr()) let remain = &lines - &cmdheight - 1 - winbottom endif let lines = s:get_preview_lines(a:lines, a:config) let height = s:get_preview_height(lines, a:config) let height = min([remain, height + 2]) if height < 0 return endif let row = position ==# 'bottom' ? winrow - 3 - height : winrow + winheight(winnr()) let title = fnamemodify(get(a:config, 'name', ''), ':.') let total = get(get(b:, 'list_status', {}), 'total', 0) if !empty(total) let title .= ' ('.line('.').'/'.total.')' endif let lnum = min([get(a:config, 'lnum', 1), len(lines)]) let opts = { \ 'relative': 'editor', \ 'width': winwidth(winnr()) - 2, \ 'borderhighlight': 'MoreMsg', \ 'highlight': 'Normal', \ 'height': height, \ 'col': 0, \ 'index': lnum - 1, \ 'row': row, \ 'border': [1,1,1,1], \ 'rounded': 1, \ 'lines': lines, \ 'scrollinside': 1, \ 'title': title, \ } let result = coc#float#create_float_win(s:pwinid, s:pbufnr, opts) if empty(result) return endif let s:pwinid = result[0] let s:pbufnr = result[1] call setwinvar(s:pwinid, 'previewwindow', 1) let topline = s:get_topline(a:config, lnum, height) call coc#window#restview(s:pwinid, lnum, topline) call s:preview_highlights(s:pwinid, s:pbufnr, a:config, 1) endfunction " Improve preview performance by reused window & buffer. " lines - list of lines " config.position - could be 'bottom' 'top' 'tab'. " config.winid - id of original window. " config.name - (optional )name of preview buffer. " config.splitRight - (optional) split to right when 1. " config.lnum - (optional) current line number " config.filetype - (optional) filetype of lines. " config.range - (optional) highlight range. with hlGroup. " config.hlGroup - (optional) highlight group. " config.maxHeight - (optional) max height of window, valid for 'bottom' & 'top' position. function! coc#list#preview(lines, config) abort let lines = s:get_preview_lines(a:lines, a:config) let winid = coc#list#get_preview(0) let bufnr = winid == -1 ? 0 : winbufnr(winid) " Try reuse buffer & window let bufnr = coc#float#create_buf(bufnr, lines) if bufnr == 0 return endif let lnum = get(a:config, 'lnum', 1) let position = get(a:config, 'position', 'bottom') let original = get(a:config, 'winid', -1) if winid == -1 let change = position != 'tab' && get(a:config, 'splitRight', 0) let curr = win_getid() if change if original && win_id2win(original) noa call win_gotoid(original) else noa wincmd t endif execute 'noa belowright vert sb '.bufnr let winid = win_getid() elseif position == 'tab' || get(a:config, 'splitRight', 0) execute 'noa belowright vert sb '.bufnr let winid = win_getid() else let mod = position == 'top' ? 'below' : 'above' let height = s:get_preview_height(lines, a:config) call s:save_views(-1) execute 'noa '.mod.' sb +resize\ '.height.' '.bufnr call s:restore_views() let winid = win_getid() endif call setbufvar(bufnr, '&synmaxcol', 500) noa call winrestview({"lnum": lnum ,"topline":s:get_topline(a:config, lnum, winheight(winid))}) call s:set_preview_options(winid) noa call win_gotoid(curr) else let height = s:get_preview_height(lines, a:config) if height > 0 if s:is_vim let curr = win_getid() noa call win_gotoid(winid) execute 'silent! noa resize '.height noa call win_gotoid(curr) else call s:save_views(winid) call nvim_win_set_height(winid, height) call s:restore_views() endif endif call coc#window#restview(winid, lnum, s:get_topline(a:config, lnum, height)) endif call s:preview_highlights(winid, bufnr, a:config, 0) endfunction function! s:preview_highlights(winid, bufnr, config, float) abort let name = fnamemodify(get(a:config, 'name', ''), ':.') let newname = s:prefix.' '.name if newname !=# bufname(a:bufnr) if s:is_vim call win_execute(a:winid, 'noa file '.fnameescape(newname), 'silent!') else silent! noa call nvim_buf_set_name(a:bufnr, newname) endif endif let filetype = get(a:config, 'filetype', '') let extname = matchstr(name, '\.\zs[^.]\+$') if empty(filetype) && !empty(extname) let filetype = get(s:filetype_map, extname, '') endif " highlights let sign_group = s:is_vim && a:float ? s:sign_popup_range : s:sign_range call win_execute(a:winid, ['syntax clear', 'call clearmatches()']) call sign_unplace(sign_group, {'buffer': a:bufnr}) let lnum = get(a:config, 'lnum', 1) if !empty(filetype) if get(g:, 'coc_list_preview_filetype', 0) call win_execute(a:winid, 'setf '.filetype) else let start = max([0, lnum - 300]) let end = min([coc#compat#buf_line_count(a:bufnr), lnum + 300]) call coc#highlight#highlight_lines(a:winid, [{'filetype': filetype, 'startLine': start, 'endLine': end}]) call win_execute(a:winid, 'syn sync fromstart') endif else call win_execute(a:winid, 'filetype detect') let ft = getbufvar(a:bufnr, '&filetype', '') if !empty(extname) && !empty(ft) let s:filetype_map[extname] = ft endif endif " selection range let targetRange = get(a:config, 'targetRange', v:null) if !empty(targetRange) for lnum in range(targetRange['start']['line'] + 1, targetRange['end']['line'] + 1) call sign_place(0, sign_group, s:current_line_hl, a:bufnr, {'lnum': lnum}) endfor endif let range = get(a:config, 'range', v:null) if !empty(range) let hlGroup = get(a:config, 'hlGroup', 'Search') call coc#highlight#match_ranges(a:winid, a:bufnr, [range], hlGroup, 10) endif endfunction function! s:get_preview_height(lines, config) abort if get(a:config, 'splitRight', 0) || get(a:config, 'position', 'bottom') == 'tab' return 0 endif let height = min([get(a:config, 'maxHeight', 10), len(a:lines), &lines - &cmdheight - 2]) return height endfunction function! s:load_buffer(name) abort if exists('*bufadd') && exists('*bufload') let bufnr = bufadd(a:name) call bufload(bufnr) return bufnr endif return 0 endfunction function! s:get_topline(config, lnum, winheight) abort let toplineStyle = get(a:config, 'toplineStyle', 'offset') if toplineStyle == 'middle' return max([1, a:lnum - a:winheight/2]) endif let toplineOffset = get(a:config, 'toplineOffset', 3) return max([1, a:lnum - toplineOffset]) endfunction function! s:set_preview_options(winid) abort call setwinvar(a:winid, '&foldmethod', 'manual') call setwinvar(a:winid, '&foldenable', 0) call setwinvar(a:winid, '&signcolumn', 'no') call setwinvar(a:winid, '&number', 1) call setwinvar(a:winid, '&cursorline', 0) call setwinvar(a:winid, '&relativenumber', 0) call setwinvar(a:winid, 'previewwindow', 1) endfunction " save views on current tabpage function! s:save_views(exclude) abort " Not work as expected when cursor becomes hidden if s:is_vim return endif for nr in range(1, winnr('$')) let winid = win_getid(nr) if winid != a:exclude && getwinvar(nr, 'previewwindow', 0) == 0 && !coc#window#is_float(winid) call win_execute(winid, 'let w:coc_list_saved_view = winsaveview()') endif endfor endfunction function! s:restore_views() abort if s:is_vim return endif for nr in range(1, winnr('$')) let saved = getwinvar(nr, 'coc_list_saved_view', v:null) if !empty(saved) let winid = win_getid(nr) call win_execute(winid, 'noa call winrestview(w:coc_list_saved_view) | unlet w:coc_list_saved_view') endif endfor endfunction function! s:set_height(height) abort let curr = winheight(0) if curr != a:height execute 'resize '.a:height endif endfunction ================================================ FILE: autoload/coc/math.vim ================================================ " support for float values function! coc#math#min(first, ...) abort let val = a:first for i in range(0, len(a:000) - 1) if a:000[i] < val let val = a:000[i] endif endfor return val endfunction ================================================ FILE: autoload/coc/notify.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:utf = has('nvim') || &encoding =~# '^utf' let s:error_icon = get(g:, 'coc_notify_error_icon', s:utf ? "\uf057" : 'E') let s:warning_icon = get(g:, 'coc_notify_warning_icon', s:utf ? "\u26a0" : 'W') let s:info_icon = get(g:, 'coc_notify_info_icon', s:utf ? "\uf06a" : 'I') let s:interval = get(g:, 'coc_notify_interval', s:is_vim ? 50 : 20) let s:phl = 'CocNotificationProgress' let s:progress_char = '─' let s:duration = 300.0 let s:winids = [] let s:fn_keys = ["\","\","\","\","\","\","\","\","\"] " Valid notify winids on current tab function! coc#notify#win_list() abort call filter(s:winids, 'coc#float#valid(v:val)') return filter(copy(s:winids), '!empty(getwinvar(v:val,"float"))') endfunction function! coc#notify#close_all() abort for winid in coc#notify#win_list() call coc#notify#close(winid) endfor endfunction " Do action for winid or first notify window with actions. function! coc#notify#do_action(...) abort let winids = a:0 > 0 ? a:000 : coc#notify#win_list() for winid in winids if coc#float#valid(winid) && getwinvar(winid, 'closing', 0) != 1 let actions = getwinvar(winid, 'actions', []) if !empty(actions) let items = map(copy(actions), '(v:key + 1).". ".v:val') let msg = join(getbufline(winbufnr(winid), 1, '$'), ' ') call coc#ui#quickpick(msg, items, {err, res -> s:on_action(err, res, winid) }) break endif endif endfor endfunction " Copy notification contents function! coc#notify#copy() abort let lines = [] for winid in coc#notify#win_list() let key = getwinvar(winid, 'key', v:null) if type(key) == v:t_string call extend(lines, json_decode(key)['lines']) endif endfor if empty(lines) echohl WarningMsg | echon 'No content to copy' | echohl None return endif call setreg('*', join(lines, "\n")) endfunction " Show source name in window function! coc#notify#show_sources() abort if !exists('*getbufline') || !exists('*appendbufline') throw "getbufline and appendbufline functions required, please upgrade your vim." endif let winids = filter(coc#notify#win_list(), 'coc#window#get_var(v:val,"closing") != 1') for winid in winids let key = getwinvar(winid, 'key', v:null) if type(key) == v:t_string let bufnr = winbufnr(winid) let obj = json_decode(key) let sourcename = get(obj, 'source', '') let lnum = get(obj, 'kind', '') ==# 'progress' ? 1 : 0 let content = get(getbufline(bufnr, lnum + 1), 0, '') if empty(sourcename) || content ==# sourcename continue endif call appendbufline(bufnr, lnum, sourcename) call coc#highlight#add_highlight(bufnr, -1, 'Title', lnum, 0, -1) call coc#float#scroll_win(winid, 0, 1) endif endfor redra endfunction function! coc#notify#close_by_source(source) abort let winids = filter(coc#notify#win_list(), 'coc#window#get_var(v:val,"closing") != 1') for winid in winids let key = getwinvar(winid, 'key', v:null) if type(key) == v:t_string let obj = json_decode(key) if get(obj, 'source', '') ==# a:source call coc#notify#close(winid) endif endif endfor endfunction " Cancel auto hide function! coc#notify#keep() abort for winid in coc#notify#win_list() call s:cancel(winid, 'close_timer') endfor endfunction " borderhighlight - border highlight [string] " maxWidth - max content width, default 60 [number] " minWidth - minimal width [number] " maxHeight - max content height, default 10 [number] " highlight - default highlight [string] " winblend - winblend [number] " timeout - auto close timeout, default 5000 [number] " title - title text " marginRight - margin right, default 10 [number] " focusable - focusable [number] " source - source name [string] " kind - kind for create icon [string] " actions - action names [string[]] " close - close button [boolean] function! coc#notify#create(lines, config) abort let actions = get(a:config, 'actions', []) if s:is_vim let actions = map(actions, 'v:val. ""') endif let key = json_encode(extend({'lines': a:lines}, a:config)) let winid = s:find_win(key) let kind = get(a:config, 'kind', '') let row = 0 " Close duplicated window if winid != -1 let row = getwinvar(winid, 'top', 0) call filter(s:winids, 'v:val != '.winid) call coc#float#close(winid, 1) endif let opts = coc#dict#pick(a:config, ['highlight', 'borderhighlight', 'focusable', 'shadow', 'close']) let border = has_key(opts, 'borderhighlight') ? [1, 1, 1, 1] : [] let icon = s:get_icon(kind, get(a:config, 'highlight', 'CocFloating')) let margin = get(a:config, 'marginRight', 10) let maxWidth = min([&columns - margin - 2, get(a:config, 'maxWidth', 80)]) if maxWidth <= 0 throw 'No enough spaces for notification' endif let lines = map(copy(a:lines), 'tr(v:val, "\t", " ")') if has_key(a:config, 'title') if !empty(border) let opts['title'] = a:config['title'] else let lines = [a:config['title']] + lines endif endif let width = max(map(copy(lines), 'strwidth(v:val)')) + (empty(icon) ? 1 : 3) if width > maxWidth let lines = coc#string#reflow(lines, maxWidth) let width = max(map(copy(lines), 'strwidth(v:val)')) + (empty(icon) ? 1 : 3) endif let highlights = [] if !empty(icon) let ic = icon['text'] if empty(lines) call add(lines, ic) else let lines[0] = ic.' '.lines[0] endif call add(highlights, {'lnum': 0, 'hlGroup': icon['hl'], 'colStart': 0, 'colEnd': strlen(ic)}) endif let actionText = join(actions, ' ') call map(lines, 'v:key == 0 ? v:val : repeat(" ", '.(empty(icon) ? 0 : 2).').v:val') let minWidth = get(a:config, 'minWidth', kind ==# 'progress' ? 30 : 10) let width = max(extend(map(lines + [get(opts, 'title', '').' '], 'strwidth(v:val)'), [minWidth, strwidth(actionText) + 1])) let width = min([maxWidth, width]) let height = min([get(a:config, 'maxHeight', 3), len(lines)]) if kind ==# 'progress' let lines = [repeat(s:progress_char, width)] + lines let height = height + 1 endif if !empty(actions) let before = max([width - strdisplaywidth(actionText), 0]) let lines = lines + [repeat(' ', before).actionText] let height = height + 1 call s:add_action_highlights(before, height - 1, highlights, actions) if s:is_vim let opts['filter'] = function('s:NotifyFilter', [len(actions)]) endif endif if row == 0 let wintop = coc#notify#get_top() let row = wintop - height - (empty(border) ? 0 : 2) - 1 if !s:is_vim && !empty(border) let row = row + 1 endif endif let col = &columns - margin - width if s:is_vim && !empty(border) let col = col - 2 endif let winblend = 60 " Avoid animate for transparent background. if get(a:config, 'winblend', 30) == 0 && empty(synIDattr(synIDtrans(hlID(get(opts, 'highlight', 'CocFloating'))), 'bg', 'gui')) let winblend = 0 endif call extend(opts, { \ 'relative': 'editor', \ 'width': width, \ 'height': height, \ 'col': col, \ 'row': row + 1, \ 'lines': lines, \ 'rounded': 1, \ 'highlights': highlights, \ 'winblend': winblend, \ 'close': s:is_vim, \ 'border': border, \ 'bufhidden': 'wipe', \ }) let result = coc#float#create_float_win(0, 0, opts) if empty(result) return endif let winid = result[0] let bufnr = result[1] call setwinvar(winid, 'right', 1) call setwinvar(winid, 'kind', 'notification') call setwinvar(winid, 'top', row) call setwinvar(winid, 'key', key) call setwinvar(winid, 'actions', actions) call setwinvar(winid, 'source', get(a:config, 'source', '')) call setwinvar(winid, 'borders', !empty(border)) call coc#float#nvim_scrollbar(winid) call add(s:winids, winid) let from = {'row': opts['row'], 'winblend': opts['winblend']} let to = {'row': row, 'winblend': get(a:config, 'winblend', 30)} call timer_start(s:interval, { -> s:animate(winid, from, to, 0)}) if kind ==# 'progress' call timer_start(s:interval, { -> s:progress(winid, width, 0, -1)}) endif if !s:is_vim call coc#compat#buf_add_keymap(bufnr, 'n', '', ':call coc#notify#nvim_click('.winid.')', { \ 'silent': v:true, \ 'nowait': v:true \ }) endif " Enable auto close if empty(actions) && kind !=# 'progress' let timer = timer_start(get(a:config, 'timeout', 10000), { -> coc#notify#close(winid)}) call setwinvar(winid, 'close_timer', timer) endif return [winid, bufnr] endfunction function! coc#notify#nvim_click(winid) abort if getwinvar(a:winid, 'closing', 0) return endif call s:cancel(a:winid, 'close_timer') let actions = getwinvar(a:winid, 'actions', []) if !empty(actions) let hls = filter(coc#highlight#get_highlights(bufnr('%'), -1), "v:val[0] ==# 'CocNotificationButton'") if empty(hls) " Something went wrong. return endif if line('.') != hls[0][1] + 1 return endif let col = col('.') let line = getline('.') for idx in range(0, len(hls) - 1) let item = hls[idx] let start_idx = coc#string#byte_index(line, item[2]) let end_idx = coc#string#byte_index(line, item[3]) if col > start_idx && col <= end_idx call coc#notify#choose(a:winid, idx) endif endfor endif endfunction function! coc#notify#on_close(winid) abort if index(s:winids, a:winid) >= 0 call filter(s:winids, 'v:val != '.a:winid) call coc#notify#reflow() endif endfunction function! coc#notify#get_top() abort let mintop = min(map(coc#notify#win_list(), 'coc#notify#get_win_top(v:val)')) if mintop != 0 return mintop endif return &lines - &cmdheight - (&laststatus == 0 ? 0 : 1 ) endfunction function! coc#notify#get_win_top(winid) abort let row = getwinvar(a:winid, 'top', 0) if row == 0 return row endif return row - (s:is_vim ? 0 : getwinvar(a:winid, 'borders', 0)) endfunction " Close with timer function! coc#notify#close(winid) abort if !coc#float#valid(a:winid) || coc#window#get_var(a:winid, 'closing', 0) == 1 return endif if !coc#window#visible(a:winid) call coc#float#close(a:winid, 1) return endif let row = coc#window#get_var(a:winid, 'top') if type(row) != v:t_number call coc#float#close(a:winid) return endif call coc#window#set_var(a:winid, 'closing', 1) call s:cancel(a:winid) let winblend = coc#window#get_var(a:winid, 'winblend', 0) let curr = s:is_vim ? {'row': row} : {'winblend': winblend} let dest = s:is_vim ? {'row': row + 1} : {'winblend': winblend == 0 ? 0 : 60} call s:animate(a:winid, curr, dest, 0, 1) endfunction function! s:add_action_highlights(before, lnum, highlights, actions) abort let colStart = a:before for text in a:actions let w = strwidth(text) let len = s:is_vim ? stridx(text, '<') : 0 call add(a:highlights, { \ 'lnum': a:lnum, \ 'hlGroup': s:is_vim ? 'CocNotificationKey' : 'CocNotificationButton', \ 'colStart': colStart + len, \ 'colEnd': colStart + w \ }) let colStart = colStart + w + 1 endfor endfunction function! s:on_action(err, idx, winid) abort if !empty(a:err) throw a:err endif if a:idx > 0 call coc#notify#choose(a:winid, a:idx - 1) endif endfunction function! s:cancel(winid, ...) abort let name = get(a:, 1, 'timer') let timer = coc#window#get_var(a:winid, name) if !empty(timer) call timer_stop(timer) call win_execute(a:winid, 'unlet w:timer', 'silent!') endif endfunction function! s:progress(winid, total, curr, index) abort if !coc#float#valid(a:winid) return endif if coc#window#visible(a:winid) let total = a:total let idx = float2nr(a:curr/5.0)%total let option = coc#float#get_options(a:winid) let width = option['width'] if idx != a:index " update percent & message let bufnr = winbufnr(a:winid) let percent = coc#window#get_var(a:winid, 'percent') let lines = [] if !empty(percent) let line = repeat(s:progress_char, width - 4).printf('%4s', percent) let total = width - 4 call add(lines, line) else call add(lines, repeat(s:progress_char, width)) endif let message = coc#window#get_var(a:winid, 'message') if !empty(message) let lines = lines + coc#string#reflow(split(message, '\v\r?\n'), width) endif if s:is_vim noa call setbufline(bufnr, 1, lines) noa call deletebufline(bufnr, len(lines) + 1, '$') else call nvim_buf_set_lines(bufnr, 0, -1, v:false, lines) endif let height = option['height'] let delta = len(lines) - height if delta > 0 && height < 3 call coc#float#change_height(a:winid, min([delta, 3 - height])) let tabnr = coc#window#tabnr(a:winid) call coc#notify#reflow(tabnr) if len(lines) > 3 call coc#float#nvim_scrollbar(a:winid) endif endif let bytes = strlen(s:progress_char) call coc#highlight#clear_highlight(bufnr, -1, 0, 1) let colStart = bytes * idx if idx + 4 <= total let colEnd = bytes * (idx + 4) call coc#highlight#add_highlight(bufnr, -1, s:phl, 0, colStart, colEnd) else let colEnd = bytes * total call coc#highlight#add_highlight(bufnr, -1, s:phl, 0, colStart, colEnd) call coc#highlight#add_highlight(bufnr, -1, s:phl, 0, 0, bytes * (idx + 4 - total)) endif endif call timer_start(s:interval, { -> s:progress(a:winid, total, a:curr + 1, idx)}) else " Not block CursorHold event call timer_start(&updatetime + 100, { -> s:progress(a:winid, a:total, a:curr, a:index)}) endif endfunction " Optional row & winblend function! s:config_win(winid, props) abort let change_row = has_key(a:props, 'row') if s:is_vim if change_row call popup_move(a:winid, {'line': a:props['row'] + 1}) endif else if change_row let [row, column] = nvim_win_get_position(a:winid) call nvim_win_set_config(a:winid, { \ 'row': a:props['row'], \ 'col': column, \ 'relative': 'editor', \ }) call s:nvim_move_related(a:winid, a:props['row']) endif call coc#float#nvim_set_winblend(a:winid, get(a:props, 'winblend', v:null)) call coc#float#nvim_refresh_scrollbar(a:winid) endif endfunction function! s:nvim_move_related(winid, row) abort let winids = coc#window#get_var(a:winid, 'related') if empty(winids) return endif for winid in winids if nvim_win_is_valid(winid) let [row, column] = nvim_win_get_position(winid) let delta = coc#window#get_var(winid, 'delta', 0) call nvim_win_set_config(winid, { \ 'row': a:row + delta, \ 'col': column, \ 'relative': 'editor', \ }) endif endfor endfunction function! s:animate(winid, from, to, prev, ...) abort if !coc#float#valid(a:winid) return endif let curr = a:prev + s:interval let percent = coc#math#min(curr / s:duration, 1) let props = s:get_props(a:from, a:to, percent) call s:config_win(a:winid, props) let close = get(a:, 1, 0) if percent < 1 call timer_start(s:interval, { -> s:animate(a:winid, a:from, a:to, curr, close)}) elseif close call filter(s:winids, 'v:val != '.a:winid) let tabnr = coc#window#tabnr(a:winid) if tabnr != -1 call coc#float#close(a:winid, 1) call coc#notify#reflow(tabnr) endif endif endfunction function! coc#notify#reflow(...) abort let tabnr = get(a:, 1, tabpagenr()) let winids = filter(copy(s:winids), 'coc#window#tabnr(v:val) == '.tabnr.' && coc#window#get_var(v:val,"closing") != 1') if empty(winids) return endif let animate = tabnr == tabpagenr() let wins = map(copy(winids), {_, val -> { \ 'winid': val, \ 'row': coc#window#get_var(val,'top',0), \ 'top': coc#window#get_var(val,'top',0) - (s:is_vim ? 0 : coc#window#get_var(val, 'borders', 0)), \ 'height': coc#float#get_height(val), \ }}) call sort(wins, {a, b -> b['top'] - a['top']}) let bottom = &lines - &cmdheight - (&laststatus == 0 ? 0 : 1 ) let moved = 0 for item in wins let winid = item['winid'] let delta = bottom - (item['top'] + item['height'] + 1) if delta != 0 call s:cancel(winid) let dest = item['row'] + delta call coc#window#set_var(winid, 'top', dest) if animate call s:move_win_timer(winid, {'row': item['row']}, {'row': dest}, 0) else call s:config_win(winid, {'row': dest}) endif let moved = moved + delta endif let bottom = item['top'] + delta endfor endfunction function! s:move_win_timer(winid, from, to, curr) abort if !coc#float#valid(a:winid) return endif if coc#window#get_var(a:winid, 'closing', 0) == 1 return endif let percent = coc#math#min(a:curr / s:duration, 1) let next = a:curr + s:interval if a:curr > 0 call s:config_win(a:winid, s:get_props(a:from, a:to, percent)) endif if percent < 1 let timer = timer_start(s:interval, { -> s:move_win_timer(a:winid, a:from, a:to, next)}) call coc#window#set_var(a:winid, 'timer', timer) endif endfunction function! s:find_win(key) abort for winid in coc#notify#win_list() if getwinvar(winid, 'key', '') ==# a:key return winid endif endfor return -1 endfunction function! s:get_icon(kind, bg) abort if a:kind ==# 'info' return {'text': s:info_icon, 'hl': coc#hlgroup#compose_hlgroup('CocInfoSign', a:bg)} endif if a:kind ==# 'warning' return {'text': s:warning_icon, 'hl': coc#hlgroup#compose_hlgroup('CocWarningSign', a:bg)} endif if a:kind ==# 'error' return {'text': s:error_icon, 'hl': coc#hlgroup#compose_hlgroup('CocErrorSign', a:bg)} endif return v:null endfunction " percent should be float function! s:get_props(from, to, percent) abort let obj = {} for key in keys(a:from) let changed = a:to[key] - a:from[key] if !s:is_vim && key ==# 'row' " Could be float let obj[key] = a:from[key] + changed * a:percent else let obj[key] = a:from[key] + float2nr(round(changed * a:percent)) endif endfor return obj endfunction function! coc#notify#choose(winid, idx) abort call s:cancel(a:winid, 'close_timer') call coc#rpc#notify('FloatBtnClick', [winbufnr(a:winid), a:idx]) call coc#notify#close(a:winid) endfunction function! s:NotifyFilter(count, winid, key) abort let max = min([a:count, 9]) for idx in range(1, max) if a:key == s:fn_keys[idx - 1] call coc#notify#choose(a:winid, idx - 1) return 1 endif endfor return 0 endfunction ================================================ FILE: autoload/coc/prompt.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:activated = 0 let s:session_names = [] let s:saved_ve = &t_ve let s:saved_cursor = &guicursor let s:gui = has('gui_running') || has('nvim') let s:char_map = { \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\":'' , \ "\":'' , \ "\":'', \ "\":'', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\<2-LeftMouse>": '<2-LeftMouse>', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ "\": '', \ } function! coc#prompt#getc() abort let c = getchar() return type(c) is 0 ? nr2char(c) : c endfunction function! coc#prompt#getchar() abort let input = coc#prompt#getc() if 1 != &iminsert return input endif "a language keymap is activated, so input must be resolved to the mapped values. let partial_keymap = mapcheck(input, 'l') while partial_keymap !=# '' let dict = maparg(input, 'l', 0, 1) if empty(dict) || get(dict, 'expr', 0) return input endif let full_keymap = get(dict, 'rhs', '') if full_keymap ==# "" && len(input) >= 3 "HACK: assume there are no keymaps longer than 3. return input elseif full_keymap ==# partial_keymap return full_keymap endif let c = coc#prompt#getc() if c ==# "\" || c ==# "\" "if the short sequence has a valid mapping, return that. if !empty(full_keymap) return full_keymap endif return input endif let input .= c let partial_keymap = mapcheck(input, 'l') endwhile return input endfunction function! coc#prompt#start_prompt(session) abort let s:session_names = s:filter(s:session_names, a:session) call add(s:session_names, a:session) if s:activated | return | endif if s:is_vim call s:start_prompt_vim() else call s:start_prompt() endif endfunction function! s:start_prompt_vim() abort call timer_start(10, {-> s:start_prompt()}) endfunction function! s:start_prompt() if s:activated | return | endif if !get(g:, 'coc_disable_transparent_cursor', 0) if s:gui if !s:is_vim && !empty(s:saved_cursor) set guicursor+=a:ver1-CocCursorTransparent/lCursor endif elseif s:is_vim set t_ve= endif endif let s:activated = 1 try while s:activated let ch = coc#prompt#getchar() if ch ==# "\" || ch ==# "\" || ch ==# "\" continue else let curr = s:current_session() let mapped = get(s:char_map, ch, ch) if !empty(curr) call coc#rpc#notify('InputChar', [curr, mapped, getcharmod()]) endif if mapped == '' let s:session_names = [] call s:reset() break endif endif endwhile catch /^Vim:Interrupt$/ let s:activated = 0 call coc#rpc#notify('InputChar', [s:current_session(), '', 0]) let s:session_names = [] call s:reset() return endtry let s:activated = 0 endfunction function! coc#prompt#stop_prompt(session) let s:session_names = s:filter(s:session_names, a:session) if len(s:session_names) return endif if s:activated let s:activated = 0 call s:reset() call feedkeys("\", 'int') endif endfunction function! coc#prompt#activated() abort return s:activated endfunction function! s:reset() abort if !get(g:, 'coc_disable_transparent_cursor',0) " neovim has bug with revert empty &guicursor if s:gui && !empty(s:saved_cursor) if !s:is_vim set guicursor+=a:ver1-Cursor/lCursor let &guicursor = s:saved_cursor endif elseif s:is_vim let &t_ve = s:saved_ve endif endif echo "" endfunction function! s:current_session() abort if empty(s:session_names) return v:null endif return s:session_names[len(s:session_names) - 1] endfunction function! s:filter(list, id) abort return filter(copy(a:list), 'v:val !=# a:id') endfunction ================================================ FILE: autoload/coc/pum.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:pum_bufnr = 0 let s:pum_winid = -1 let s:pum_index = -1 let s:pum_size = 0 " word of complete item inserted let s:inserted = 0 let s:virtual_text = 0 let s:virtual_text_ns = coc#highlight#create_namespace('pum-virtual') " bufnr, &indentkeys let s:saved_indenetkeys = [] let s:saved_textwidth = [] let s:prop_id = 0 let s:reversed = 0 let s:check_hl_group = 0 let s:start_col = -1 if s:is_vim if empty(prop_type_get('CocPumVirtualText')) call prop_type_add('CocPumVirtualText', {'highlight': 'CocPumVirtualText'}) endif endif function! coc#pum#has_item_selected() abort return coc#pum#visible() && s:pum_index != -1 endfunction function! coc#pum#visible() abort if s:pum_winid == -1 return 0 endif " getwinvar check current tab only. return getwinvar(s:pum_winid, 'float', 0) == 1 endfunction function! coc#pum#winid() abort return s:pum_winid endfunction function! coc#pum#close_detail() abort let winid = coc#float#get_float_by_kind('pumdetail') if winid call coc#float#close(winid, 1) endif endfunction " kind, and skipRequest (default to false) function! coc#pum#close(...) abort if coc#pum#visible() let inserted = 0 let kind = get(a:, 1, '') if kind ==# 'cancel' let input = getwinvar(s:pum_winid, 'input', '') let s:pum_index = -1 let inserted = s:insert_word(input, 1) call s:on_pum_change(0) elseif kind ==# 'confirm' let words = getwinvar(s:pum_winid, 'words', []) if s:pum_index >= 0 let word = get(words, s:pum_index, '') let inserted = s:insert_word(word, 1) " have to restore here, so that TextChangedI can trigger indent. call s:restore_indentkeys() endif endif call s:close_pum() if !get(a:, 2, 0) " Needed to wait TextChangedI fired if inserted call timer_start(0, {-> coc#rpc#request('stopCompletion', [kind])}) else call coc#rpc#request('stopCompletion', [kind]) endif endif endif return '' endfunction function! coc#pum#select_confirm() abort if coc#pum#visible() if s:pum_index < 0 let s:pum_index = 0 call s:on_pum_change(0) endif " Avoid change of text not allowed return "\=coc#pum#close('confirm')\" endif return '' endfunction function! coc#pum#_close() abort if coc#pum#visible() call s:close_pum() if s:is_vim call timer_start(0, {-> execute('redraw')}) endif endif endfunction function! coc#pum#_one_more() abort if coc#pum#visible() let parts = getwinvar(s:pum_winid, 'parts', []) let start = strlen(parts[0]) let input = strpart(getline('.'), start, col('.') - 1 - start) let words = getwinvar(s:pum_winid, 'words', []) let word = get(words, s:pum_index == -1 ? 0 : s:pum_index, '') if !empty(word) && strcharpart(word, 0, strchars(input)) ==# input let ch = strcharpart(word, strchars(input), 1) if !empty(ch) call feedkeys(ch, "nt") endif endif endif return '' endfunction function! coc#pum#_insert() abort if coc#pum#visible() if s:pum_index >= 0 let words = getwinvar(s:pum_winid, 'words', []) let word = get(words, s:pum_index, '') call s:insert_word(word, 1) call s:restore_indentkeys() endif doautocmd TextChangedI call s:close_pum() call timer_start(0, {-> coc#rpc#request('stopCompletion', [''])}) endif return '' endfunction function! coc#pum#insert() abort return "\=coc#pum#_insert()\" endfunction " Add one more character from the matched complete item(or first one), " the word should starts with input, the same as vim's CTRL-L behavior. function! coc#pum#one_more() abort return "\=coc#pum#_one_more()\" endfunction function! coc#pum#next(insert) abort return "\=coc#pum#_navigate(1,".a:insert.")\" endfunction function! coc#pum#prev(insert) abort return "\=coc#pum#_navigate(0,".a:insert.")\" endfunction function! coc#pum#stop() abort return "\=coc#pum#close()\" endfunction function! coc#pum#cancel() abort return "\=coc#pum#close('cancel')\" endfunction function! coc#pum#confirm() abort return "\=coc#pum#close('confirm')\" endfunction function! coc#pum#select(index, insert, confirm) abort if coc#pum#visible() if a:index == -1 call coc#pum#close('cancel') return '' endif if a:index < 0 || a:index >= s:pum_size throw 'index out of range ' . a:index endif if a:confirm if s:pum_index != a:index let s:pum_index = a:index let s:inserted = 1 call s:on_pum_change(0) endif call coc#pum#close('confirm') else call s:select_by_index(a:index, a:insert) endif endif return '' endfunction function! coc#pum#info() abort let bufnr = winbufnr(s:pum_winid) let words = getwinvar(s:pum_winid, 'words', []) let word = s:pum_index < 0 ? '' : get(words, s:pum_index, '') let base = { \ 'word': word, \ 'index': s:pum_index, \ 'size': s:pum_size, \ 'startcol': s:start_col, \ 'inserted': s:pum_index >=0 && s:inserted ? v:true : v:false, \ 'reversed': s:reversed ? v:true : v:false, \ } if s:is_vim let pos = popup_getpos(s:pum_winid) let border = has_key(popup_getoptions(s:pum_winid), 'border') let add = pos['scrollbar'] && border ? 1 : 0 return extend(base, { \ 'scrollbar': pos['scrollbar'], \ 'row': pos['line'] - 1, \ 'col': pos['col'] - 1, \ 'width': pos['width'] + add, \ 'height': pos['height'], \ 'border': border, \ }) else let scrollbar = coc#float#get_related(s:pum_winid, 'scrollbar') let winid = coc#float#get_related(s:pum_winid, 'border', s:pum_winid) let pos = nvim_win_get_position(winid) return extend(base, { \ 'scrollbar': scrollbar && nvim_win_is_valid(scrollbar) ? 1 : 0, \ 'row': pos[0], \ 'col': pos[1], \ 'width': nvim_win_get_width(winid), \ 'height': nvim_win_get_height(winid), \ 'border': winid != s:pum_winid, \ }) endif endfunction function! coc#pum#scroll(forward) abort if coc#pum#visible() let height = s:get_height(s:pum_winid) if s:pum_size > height call timer_start(1, { -> s:scroll_pum(a:forward, height, s:pum_size)}) endif endif " Required on old version vim/neovim. return "\" endfunction function! s:get_height(winid) abort if s:is_vim return get(popup_getpos(a:winid), 'core_height', 0) endif return nvim_win_get_height(a:winid) endfunction function! s:scroll_pum(forward, height, size) abort let topline = s:get_topline(s:pum_winid) if !a:forward && topline == 1 if s:pum_index >= 0 call s:select_line(s:pum_winid, 1) call s:on_pum_change(1) endif return endif if a:forward && topline + a:height - 1 >= a:size if s:pum_index >= 0 call s:select_line(s:pum_winid, a:size) call s:on_pum_change(1) endif return endif call coc#float#scroll_win(s:pum_winid, a:forward, a:height) if s:pum_index >= 0 let lnum = s:pum_index + 1 let topline = s:get_topline(s:pum_winid) if lnum >= topline && lnum <= topline + a:height - 1 return endif call s:select_line(s:pum_winid, topline) call s:on_pum_change(1) endif endfunction function! s:get_topline(winid) abort if s:is_vim let pos = popup_getpos(a:winid) return pos['firstline'] endif let info = getwininfo(a:winid)[0] return info['topline'] endfunction function! coc#pum#_navigate(next, insert) abort if coc#pum#visible() call s:save_indentkeys() let index = s:get_index(a:next) call s:select_by_index(index, a:insert) call coc#rpc#notify('PumNavigate', [bufnr('%')]) endif return '' endfunction function! s:select_by_index(index, insert) abort let lnum = a:index == -1 ? 0 : s:index_to_lnum(a:index) call s:set_cursor(s:pum_winid, lnum) if !s:is_vim call coc#float#nvim_scrollbar(s:pum_winid) endif if a:insert let s:inserted = a:index >= 0 if a:index < 0 let input = getwinvar(s:pum_winid, 'input', '') call s:insert_word(input, 0) call coc#pum#close_detail() else let words = getwinvar(s:pum_winid, 'words', []) let word = get(words, a:index, '') call s:insert_word(word, 0) endif endif call s:on_pum_change(1) endfunction function! s:get_index(next) abort if a:next let index = s:pum_index + 1 == s:pum_size ? -1 : s:pum_index + 1 else let index = s:pum_index == -1 ? s:pum_size - 1 : s:pum_index - 1 endif return index endfunction function! s:insert_word(word, finish) abort if s:start_col != -1 && mode() ==# 'i' " Not insert same characters let inserted = strpart(getline('.'), s:start_col, col('.') - 1) if inserted !=# a:word " avoid auto wrap using 'textwidth' if !a:finish && &textwidth > 0 let textwidth = &textwidth noa setl textwidth=0 call timer_start(0, { -> execute('noa setl textwidth='.textwidth)}) endif let saved_completeopt = &completeopt noa set completeopt=noinsert,noselect noa call complete(s:start_col + 1, [{ 'empty': v:true, 'word': a:word }]) noa call feedkeys("\\\", 'in') call timer_start(0, { -> execute('noa set completeopt='.saved_completeopt)}) return 1 endif endif return 0 endfunction " Replace from col to cursor col with new characters function! coc#pum#replace(col, insert, delta) abort if a:delta == 1 call feedkeys("\", 'in') endif let saved_completeopt = &completeopt noa set completeopt=noinsert,noselect noa call complete(a:col, [{ 'empty': v:true, 'word': a:insert }]) noa call feedkeys("\\\", 'n') execute 'noa set completeopt='.saved_completeopt endfunction " create or update pum with lines, CompleteOption and config. " return winid & dimension function! coc#pum#create(lines, opt, config) abort if mode() !=# 'i' || a:opt['line'] != line('.') return endif let len = col('.') - a:opt['col'] - 1 if len < 0 return endif let input = len == 0 ? '' : strpart(getline('.'), a:opt['col'], len) if input !=# a:opt['input'] return endif let config = s:get_pum_dimension(a:lines, a:opt['col'], a:config) if empty(config) return endif let s:reversed = get(a:config, 'reverse', 0) && config['row'] < 0 let s:virtual_text = get(a:opt, 'virtualText', v:false) let s:pum_size = len(a:lines) let s:pum_index = a:opt['index'] let lnum = s:index_to_lnum(s:pum_index) call extend(config, { \ 'lines': s:reversed ? reverse(copy(a:lines)) : a:lines, \ 'relative': 'cursor', \ 'nopad': 1, \ 'cursorline': 1, \ 'index': lnum - 1, \ 'focusable': v:false \ }) call extend(config, coc#dict#pick(a:config, ['highlight', 'rounded', 'highlights', 'winblend', 'shadow', 'border', 'borderhighlight', 'title'])) if s:reversed for item in config['highlights'] let item['lnum'] = s:pum_size - item['lnum'] - 1 endfor endif if empty(get(config, 'winblend', 0)) && exists('&pumblend') let config['winblend'] = &pumblend endif let result = coc#float#create_float_win(s:pum_winid, s:pum_bufnr, config) if empty(result) return endif let s:inserted = 0 let s:pum_winid = result[0] let s:pum_bufnr = result[1] let s:start_col = a:opt['startcol'] call setwinvar(s:pum_winid, 'above', config['row'] < 0) let firstline = s:get_firstline(lnum, s:pum_size, config['height']) if s:is_vim call popup_setoptions(s:pum_winid, { 'firstline': firstline }) else call win_execute(s:pum_winid, 'call winrestview({"lnum":'.lnum.',"topline":'.firstline.'})') endif call coc#dialog#place_sign(s:pum_bufnr, s:pum_index == -1 ? 0 : lnum) " content before col and content after cursor let linetext = getline('.') let parts = [strpart(linetext, 0, s:start_col), strpart(linetext, col('.') - 1)] let input = strpart(getline('.'), s:start_col, col('.') - 1 - s:start_col) call setwinvar(s:pum_winid, 'input', input) call setwinvar(s:pum_winid, 'parts', parts) call setwinvar(s:pum_winid, 'words', a:opt['words']) call setwinvar(s:pum_winid, 'kind', 'pum') if !s:is_vim if s:pum_size > config['height'] call timer_start(0,{ -> coc#float#nvim_scrollbar(s:pum_winid)}) else call coc#float#close_related(s:pum_winid, 'scrollbar') endif endif call s:on_pum_change(0) endfunction function! s:save_indentkeys() abort let bufnr = bufnr('%') if !empty(&indentexpr) && get(s:saved_indenetkeys, 0, 0) != bufnr let s:saved_indenetkeys = [bufnr, &indentkeys] execute 'setl indentkeys=' endif endfunction function! s:get_firstline(lnum, total, height) abort if a:lnum <= a:height return 1 endif return min([a:total - a:height + 1, a:lnum - (a:height*2/3)]) endfunction function! s:on_pum_change(move) abort if s:virtual_text if s:inserted call coc#pum#clear_vtext() else call s:insert_virtual_text() endif endif let ev = extend(coc#pum#info(), {'move': a:move ? v:true : v:false}) call coc#rpc#notify('CocAutocmd', ['MenuPopupChanged', ev, win_screenpos(winnr())[0] + winline() - 2]) endfunction function! s:index_to_lnum(index) abort if s:reversed if a:index <= 0 return s:pum_size endif return s:pum_size - a:index endif return max([1, a:index + 1]) endfunction function! s:get_pum_dimension(lines, col, config) abort let linecount = len(a:lines) let [lineIdx, colIdx] = coc#cursor#screen_pos() let bh = empty(get(a:config, 'border', [])) ? 0 : 2 let columns = &columns let pumwidth = max([15, exists('&pumwidth') ? &pumwidth : 0]) let width = min([columns, max([pumwidth, a:config['width']])]) let vh = &lines - &cmdheight - 1 - !empty(&tabline) if vh <= 0 return v:null endif let pumheight = empty(&pumheight) ? vh : &pumheight let showTop = getwinvar(s:pum_winid, 'above', v:null) if type(showTop) != v:t_number if vh - lineIdx - bh - 1 < min([pumheight, linecount]) && vh - lineIdx < min([10, vh/2]) let showTop = 1 else let showTop = 0 endif endif let height = showTop ? min([lineIdx - bh - !empty(&tabline), linecount, pumheight]) : min([vh - lineIdx - bh - 1, linecount, pumheight]) if height <= 0 return v:null endif " should use strdiplaywidth here let text = strpart(getline('.'), a:col, col('.') - 1 - a:col) let col = - strdisplaywidth(text, a:col) - 1 let row = showTop ? - height : 1 let delta = colIdx + col if width > pumwidth && delta + width > columns let width = max([columns - delta, pumwidth]) endif if delta < 0 let col = col - delta elseif delta + width > columns let col = max([-colIdx, col - (delta + width - columns)]) endif return { \ 'row': row, \ 'col': col, \ 'width': width, \ 'height': height \ } endfunction " can't use coc#dialog#set_cursor on vim8, don't know why function! s:set_cursor(winid, line) abort if s:is_vim let pos = popup_getpos(a:winid) let core_height = pos['core_height'] let lastline = pos['firstline'] + core_height - 1 if a:line > lastline call popup_setoptions(a:winid, { \ 'firstline': pos['firstline'] + a:line - lastline, \ }) elseif a:line < pos['firstline'] if s:reversed call popup_setoptions(a:winid, { \ 'firstline': a:line == 0 ? s:pum_size - core_height + 1 : a:line - core_height + 1, \ }) else call popup_setoptions(a:winid, { \ 'firstline': max([1, a:line]), \ }) endif endif endif call s:select_line(a:winid, a:line) endfunction function! s:select_line(winid, line) abort let s:pum_index = s:reversed ? (a:line == 0 ? -1 : s:pum_size - a:line) : a:line - 1 let lnum = s:reversed ? (a:line == 0 ? s:pum_size : a:line) : max([1, a:line]) if s:is_vim call win_execute(a:winid, 'exe '.lnum) else call nvim_win_set_cursor(a:winid, [lnum, 0]) endif call coc#dialog#place_sign(s:pum_bufnr, a:line == 0 ? 0 : lnum) endfunction function! s:insert_virtual_text() abort let bufnr = bufnr('%') if !s:virtual_text || s:pum_index < 0 call coc#pum#clear_vtext() else " Check if could create let insert = '' let line = line('.') - 1 let words = getwinvar(s:pum_winid, 'words', []) let word = get(words, s:pum_index, '') let input = strpart(getline('.'), s:start_col, col('.') - 1 - s:start_col) if strlen(word) > strlen(input) && strcharpart(word, 0, strchars(input)) ==# input let insert = strcharpart(word, strchars(input)) endif if s:is_vim if s:prop_id != 0 call prop_remove({'id': s:prop_id}, line + 1, line + 1) endif if !empty(insert) let s:prop_id = prop_add(line + 1, col('.'), { \ 'text': insert, \ 'type': 'CocPumVirtualText' \ }) endif else call nvim_buf_clear_namespace(bufnr, s:virtual_text_ns, line, line + 1) if !empty(insert) let opts = { \ 'hl_mode': 'combine', \ 'virt_text': [[insert, 'CocPumVirtualText']], \ 'virt_text_pos': 'overlay', \ 'virt_text_win_col': virtcol('.') - 1, \ } call nvim_buf_set_extmark(bufnr, s:virtual_text_ns, line, col('.') - 1, opts) endif endif endif endfunction function! coc#pum#clear_vtext() abort if s:is_vim if s:prop_id != 0 call prop_remove({'id': s:prop_id}) endif let s:prop_id = 0 else call nvim_buf_clear_namespace(bufnr('%'), s:virtual_text_ns, 0, -1) endif endfunction function! s:close_pum() abort call coc#pum#clear_vtext() call coc#float#close(s:pum_winid, 1) let s:pum_winid = 0 let s:pum_size = 0 let winid = coc#float#get_float_by_kind('pumdetail') if winid call coc#float#close(winid, 1) endif call s:restore_indentkeys() endfunction function! s:restore_indentkeys() abort if get(s:saved_indenetkeys, 0, 0) == bufnr('%') call setbufvar(s:saved_indenetkeys[0], '&indentkeys', get(s:saved_indenetkeys, 1, '')) let s:saved_indenetkeys = [] endif endfunction ================================================ FILE: autoload/coc/rpc.vim ================================================ scriptencoding utf-8 let s:is_win = has("win32") || has("win64") let s:client = v:null let s:name = 'coc' let s:is_vim = !has('nvim') let s:chan_id = 0 let s:root = expand(':h:h:h') function! coc#rpc#start_server() let test = get(g:, 'coc_node_env', '') ==# 'test' if test && !s:is_vim && !exists('$COC_NVIM_REMOTE_ADDRESS') " server already started, chan_id could be available later let s:client = coc#client#create(s:name, []) let s:client['running'] = s:chan_id != 0 let s:client['chan_id'] = s:chan_id return endif if exists('$COC_NVIM_REMOTE_ADDRESS') let address = $COC_NVIM_REMOTE_ADDRESS if s:is_vim let s:client = coc#client#create(s:name, []) " TODO don't know if vim support named pipe on windows. let address = address =~# ':\d\+$' ? address : 'unix:'.address let channel = ch_open(address, { \ 'mode': 'json', \ 'close_cb': {channel -> s:on_channel_close()}, \ 'noblock': 1, \ 'timeout': 1000, \ }) if ch_status(channel) == 'open' let s:client['running'] = 1 let s:client['channel'] = channel endif else let s:client = coc#client#create(s:name, []) try let mode = address =~# ':\d\+$' ? 'tcp' : 'pipe' let chan_id = sockconnect(mode, address, { 'rpc': 1 }) if chan_id > 0 let s:client['running'] = 1 let s:client['chan_id'] = chan_id endif catch /connection\ refused/ " ignore endtry endif if !s:client['running'] echohl Error | echom '[coc.nvim] Unable connect to '.address.' from variable $COC_NVIM_REMOTE_ADDRESS' | echohl None elseif !test let logfile = exists('$NVIM_COC_LOG_FILE') ? $NVIM_COC_LOG_FILE : '' let loglevel = exists('$NVIM_COC_LOG_LEVEL') ? $NVIM_COC_LOG_LEVEL : '' let runtimepath = join(coc#compat#list_runtime_paths(), ",") let data = [coc#util#win32unix_to_node(s:root), coc#util#get_data_home(), coc#util#get_config_home(), logfile, loglevel, runtimepath] if s:is_vim call ch_sendraw(s:client['channel'], json_encode(data)."\n") else call call('rpcnotify', [s:client['chan_id'], 'init'] + data) endif endif return endif if empty(s:client) let cmd = coc#util#job_command() if empty(cmd) | return | endif let $COC_VIMCONFIG = coc#util#get_config_home() let $COC_DATA_HOME = coc#util#get_data_home() let s:client = coc#client#create(s:name, cmd) endif if !coc#client#is_running('coc') call s:client['start']() endif call s:check_vim_enter() endfunction function! coc#rpc#started() abort return !empty(s:client) endfunction function! coc#rpc#ready() if empty(s:client) || s:client['running'] == 0 return 0 endif return 1 endfunction " Used for test on neovim only function! coc#rpc#set_channel(chan_id) abort let s:chan_id = a:chan_id let s:client['running'] = a:chan_id != 0 let s:client['chan_id'] = a:chan_id endfunction function! coc#rpc#get_channel() abort if empty(s:client) return v:null endif return coc#client#get_channel(s:client) endfunction function! coc#rpc#kill() let pid = get(g:, 'coc_process_pid', 0) if !pid | return | endif if s:is_win call system('taskkill /PID '.pid) else call system('kill -9 '.pid) endif endfunction function! coc#rpc#show_errors() let client = coc#client#get_client('coc') if !empty(client) let lines = get(client, 'stderr', []) keepalt new +setlocal\ buftype=nofile [Stderr of coc.nvim] setl noswapfile wrap bufhidden=wipe nobuflisted nospell call append(0, lines) exe "normal! z" . len(lines) . "\" exe "normal! gg" endif endfunction function! coc#rpc#stop() if empty(s:client) return endif try if s:is_vim call job_stop(ch_getjob(s:client['channel']), 'term') else call jobstop(s:client['chan_id']) endif catch /.*/ " ignore endtry endfunction function! coc#rpc#restart() if empty(s:client) call coc#rpc#start_server() else call coc#highlight#clear_all() call coc#ui#sign_unplace() call coc#float#close_all() call coc#clearGroups('coc_dynamic_') call coc#rpc#request('detach', []) if !empty(get(g:, 'coc_status', '')) unlet g:coc_status endif let g:coc_service_initialized = 0 sleep 100m if exists('$COC_NVIM_REMOTE_ADDRESS') call coc#rpc#close_connection() sleep 100m call coc#rpc#start_server() else let s:client['command'] = coc#util#job_command() call coc#client#restart(s:name) call s:check_vim_enter() endif echohl MoreMsg | echom 'starting coc.nvim service' | echohl None endif endfunction function! coc#rpc#close_connection() abort let channel = coc#rpc#get_channel() if channel == v:null return endif if s:is_vim " Unlike neovim, vim not close the socket as expected. call ch_close(channel) else call chanclose(channel) endif let s:client['running'] = 0 let s:client['channel'] = v:null let s:client['chan_id'] = 0 endfunction function! coc#rpc#request(method, args) abort if !coc#rpc#ready() return '' endif return s:client['request'](a:method, a:args) endfunction function! coc#rpc#notify(method, args) abort if !coc#rpc#ready() return '' endif call s:client['notify'](a:method, a:args) return '' endfunction function! coc#rpc#request_async(method, args, cb) abort if !coc#rpc#ready() return call(a:cb, ['coc.nvim service not started.']) endif call s:client['request_async'](a:method, a:args, a:cb) endfunction " receive async response function! coc#rpc#async_response(id, resp, isErr) abort if empty(s:client) return endif call coc#client#on_response(s:name, a:id, a:resp, a:isErr) endfunction " send async response to server function! coc#rpc#async_request(id, method, args) let l:Cb = {err, ... -> coc#rpc#notify('nvim_async_response_event', [a:id, err, get(a:000, 0, v:null)])} let args = a:args + [l:Cb] try call call(a:method, args) catch /.*/ call coc#rpc#notify('nvim_async_response_event', [a:id, v:exception, v:null]) endtry endfunction function! s:check_vim_enter() abort if s:client['running'] && v:vim_did_enter call coc#rpc#notify('VimEnter', [join(coc#compat#list_runtime_paths(), ",")]) endif endfunction " Used on vim and remote address only function! s:on_channel_close() abort if get(g:, 'coc_node_env', '') !=# 'test' echohl Error | echom '[coc.nvim] channel closed' | echohl None endif if !empty(s:client) let s:client['running'] = 0 let s:client['channel'] = v:null let s:client['async_req_id'] = 1 endif endfunction ================================================ FILE: autoload/coc/snippet.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:map_next = 1 let s:map_prev = 1 function! coc#snippet#_select_mappings(bufnr) if !get(g:, 'coc_selectmode_mapping', 1) return endif redir => mappings silent! smap redir END for map in map(filter(split(mappings, '\n'), \ "v:val !~# '^s' && v:val !~# '^\\a*\\s*<\\S\\+>'"), \ "matchstr(v:val, '^\\a*\\s*\\zs\\S\\+')") silent! execute 'sunmap' map call coc#compat#buf_del_keymap(a:bufnr, 's', map) endfor " same behaviour of ultisnips snoremap c snoremap c snoremap c snoremap "_c endfunction function! coc#snippet#show_choices(lnum, col, position, input) abort call coc#snippet#move(a:position) call CocActionAsync('startCompletion', { \ 'source': '$words', \ 'col': a:col \ }) redraw endfunction function! coc#snippet#enable(...) let bufnr = get(a:, 1, bufnr('%')) if getbufvar(bufnr, 'coc_snippet_active', 0) == 1 return endif let complete = get(a:, 2, 0) call setbufvar(bufnr, 'coc_snippet_active', 1) call coc#snippet#_select_mappings(bufnr) let nextkey = get(g:, 'coc_snippet_next', '') let prevkey = get(g:, 'coc_snippet_prev', '') if maparg(nextkey, 'i') =~# 'snippet' let s:map_next = 0 endif if maparg(prevkey, 'i') =~# 'snippet' let s:map_prev = 0 endif if !empty(nextkey) if s:map_next call s:buf_add_keymap(bufnr, 'i', nextkey, ":call coc#snippet#jump(1, ".complete.")") endif call s:buf_add_keymap(bufnr, 's', nextkey, ":call coc#snippet#jump(1, ".complete.")") endif if !empty(prevkey) if s:map_prev call s:buf_add_keymap(bufnr, 'i', prevkey, ":call coc#snippet#jump(0, ".complete.")") endif call s:buf_add_keymap(bufnr, 's', prevkey, ":call coc#snippet#jump(0, ".complete.")") endif endfunction function! coc#snippet#disable(...) let bufnr = get(a:, 1, bufnr('%')) if getbufvar(bufnr, 'coc_snippet_active', 0) == 0 return endif call setbufvar(bufnr, 'coc_snippet_active', 0) let nextkey = get(g:, 'coc_snippet_next', '') let prevkey = get(g:, 'coc_snippet_prev', '') if s:map_next call coc#compat#buf_del_keymap(bufnr, 'i', nextkey) endif if s:map_prev call coc#compat#buf_del_keymap(bufnr, 'i', prevkey) endif call coc#compat#buf_del_keymap(bufnr, 's', nextkey) call coc#compat#buf_del_keymap(bufnr, 's', prevkey) endfunction function! coc#snippet#prev() abort call coc#rpc#request('snippetPrev', []) return '' endfunction function! coc#snippet#next() abort call coc#rpc#request('snippetNext', []) return '' endfunction function! coc#snippet#jump(direction, complete) abort if a:direction == 1 && a:complete if pumvisible() let pre = exists('*complete_info') && complete_info()['selected'] == -1 ? "\" : '' call feedkeys(pre."\", 'in') return '' endif if coc#pum#visible() " Discard the return value, otherwise weird characters will be inserted call coc#pum#close('confirm') return '' endif endif call coc#pum#close() call coc#rpc#request(a:direction == 1 ? 'snippetNext' : 'snippetPrev', []) return '' endfunction function! coc#snippet#select(start, end, text) abort if coc#pum#visible() call coc#pum#close() endif if mode() ==? 's' call feedkeys("\", 'in') endif if &selection ==# 'exclusive' let cursor = coc#snippet#to_cursor(a:start) call cursor([cursor[0], cursor[1]]) let cmd = '' let cmd .= mode()[0] ==# 'i' ? "\".(col('.') == 1 ? '' : 'l') : '' let cmd .= printf('zvv%s', strchars(a:text) . 'l') let cmd .= "\" else let cursor = coc#snippet#to_cursor(a:end) call cursor([cursor[0], cursor[1] - 1]) let len = strchars(a:text) - 1 let cmd = '' let cmd .= mode()[0] ==# 'i' ? "\".(col('.') == 1 ? '' : 'l') : '' let cmd .= printf('zvv%s', len > 0 ? len . 'h' : '') let cmd .= "o\" endif if s:is_vim " Can't use 't' since the code of can be changed. call feedkeys(cmd, 'n') else call feedkeys(cmd, 'nt') endif endfunction function! coc#snippet#move(position) abort let m = mode() if m ==? 's' call feedkeys("\", 'in') endif let pos = coc#snippet#to_cursor(a:position) call cursor(pos) if pos[1] > strlen(getline(pos[0])) startinsert! else startinsert endif endfunction function! coc#snippet#to_cursor(position) abort let line = getline(a:position.line + 1) if line is v:null return [a:position.line + 1, a:position.character + 1] endif return [a:position.line + 1, coc#string#byte_index(line, a:position.character) + 1] endfunction function! s:buf_add_keymap(bufnr, mode, lhs, rhs) abort let opts = {'nowait': v:true, 'silent': v:true} call coc#compat#buf_add_keymap(a:bufnr, a:mode, a:lhs, a:rhs, opts) endfunction ================================================ FILE: autoload/coc/string.vim ================================================ scriptencoding utf-8 function! coc#string#last_character(line) abort return strcharpart(a:line, strchars(a:line) - 1, 1) endfunction " Get utf16 code unit index from col (0 based) function! coc#string#character_index(line, byteIdx) abort if a:byteIdx <= 0 return 0 endif let i = 0 for char in split(strpart(a:line, 0, a:byteIdx), '\zs') let i += char2nr(char) > 65535 ? 2 : 1 endfor return i endfunction " Convert utf16 character index to byte index function! coc#string#byte_index(line, character) abort if a:character <= 0 return 0 endif " code unit index let i = 0 let len = 0 for char in split(a:line, '\zs') let i += char2nr(char) > 65535 ? 2 : 1 let len += strlen(char) if i >= a:character break endif endfor return len endfunction function! coc#string#character_length(text) abort let i = 0 for char in split(a:text, '\zs') let i += char2nr(char) > 65535 ? 2 : 1 endfor return i endfunction function! coc#string#reflow(lines, width) abort let lines = [] let currlen = 0 let parts = [] for line in a:lines for part in split(line, '\s\+') let w = strwidth(part) if currlen + w + 1 >= a:width if len(parts) > 0 call add(lines, join(parts, ' ')) endif if w >= a:width call add(lines, part) let currlen = 0 let parts = [] else let currlen = w let parts = [part] endif continue endif call add(parts, part) let currlen = currlen + w + 1 endfor endfor if len(parts) > 0 call add(lines, join(parts, ' ')) endif return empty(lines) ? [''] : lines endfunction " Used when 'wrap' and 'linebreak' is enabled function! coc#string#content_height(lines, width) abort let len = 0 let pattern = empty(&breakat) ? '.\zs' : '['.substitute(&breakat, '\([\[\]\-]\)', '\\\1', 'g').']\zs' for line in a:lines if strwidth(line) <= a:width let len += 1 else let currlen = 0 for part in split(line, pattern) let wl = strwidth(part) if currlen == 0 && wl > 0 let len += 1 endif let delta = currlen + wl - a:width if delta >= 0 let len = len + (delta > 0) let currlen = delta == 0 ? 0 : wl if wl >= a:width let currlen = wl%a:width let len += float2nr(ceil(wl/(a:width + 0.0))) - (currlen == 0) endif else let currlen = currlen + wl endif endfor endif endfor return len endfunction " insert inserted to line at position, use ... when result is too long " line should only contains character has strwidth equals 1 function! coc#string#compose(line, position, inserted) abort let width = strwidth(a:line) let text = a:inserted let res = a:line let need_truncate = a:position + strwidth(text) + 1 > width if need_truncate let remain = width - a:position - 3 if remain < 2 " use text for full line, use first & end of a:line, ignore position let res = strcharpart(a:line, 0, 1) let w = strwidth(res) for i in range(strchars(text)) let c = strcharpart(text, i, 1) let a = strwidth(c) if w + a <= width - 1 let w = w + a let res = res . c endif endfor let res = res.strcharpart(a:line, w) else let res = strcharpart(a:line, 0, a:position) let w = strwidth(res) for i in range(strchars(text)) let c = strcharpart(text, i, 1) let a = strwidth(c) if w + a <= width - 3 let w = w + a let res = res . c endif endfor let res = res.'..' let w = w + 2 let res = res . strcharpart(a:line, w) endif else let first = strcharpart(a:line, 0, a:position) let res = first . text . strcharpart(a:line, a:position + strwidth(text)) endif return res endfunction ================================================ FILE: autoload/coc/task.vim ================================================ " ============================================================================ " Description: Manage long running tasks. " Author: Qiming Zhao " Licence: Anti 966 licence " Version: 0.1 " Last Modified: Dec 12, 2020 " ============================================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:running_task = {} " neovim emit strings that part of lines. let s:out_remain_text = {} let s:err_remain_text = {} function! coc#task#start(id, opts) if coc#task#running(a:id) call coc#task#stop(a:id) endif let cmd = [a:opts['cmd']] + get(a:opts, 'args', []) let cwd = get(a:opts, 'cwd', getcwd()) let env = get(a:opts, 'env', {}) " cmd args cwd pty if s:is_vim let options = { \ 'cwd': cwd, \ 'noblock' : 1, \ 'err_mode': 'nl', \ 'out_mode': 'nl', \ 'err_cb': {channel, message -> s:on_stderr(a:id, [message])}, \ 'out_cb': {channel, message -> s:on_stdout(a:id, [message])}, \ 'exit_cb': {channel, code -> s:on_exit(a:id, code)}, \ 'env': env, \} if get(a:opts, 'pty', 0) let options['pty'] = 1 endif let job = job_start(cmd, options) let status = job_status(job) if status !=# 'run' echohl Error | echom 'Failed to start '.a:id.' task' | echohl None return v:false endif let s:running_task[a:id] = job else let options = { \ 'cwd': cwd, \ 'on_stderr': {channel, msgs -> s:on_stderr(a:id, msgs)}, \ 'on_stdout': {channel, msgs -> s:on_stdout(a:id, msgs)}, \ 'on_exit': {channel, code -> s:on_exit(a:id, code)}, \ 'detach': get(a:opts, 'detach', 0), \ 'env': env, \} if get(a:opts, 'pty', 0) let options['pty'] = 1 endif let chan_id = jobstart(cmd, options) if chan_id <= 0 echohl Error | echom 'Failed to start '.a:id.' task' | echohl None return v:false endif let s:running_task[a:id] = chan_id endif return v:true endfunction function! coc#task#stop(id) let job = get(s:running_task, a:id, v:null) if !job | return | endif if s:is_vim call job_stop(job, 'term') else call jobstop(job) endif sleep 50m let running = coc#task#running(a:id) if running echohl Error | echom 'job '.a:id. ' stop failed.' | echohl None endif endfunction function! s:on_exit(id, code) abort if get(g:, 'coc_vim_leaving', 0) | return | endif if !s:is_vim let s:out_remain_text[a:id] = '' let s:err_remain_text[a:id] = '' endif if has_key(s:running_task, a:id) call remove(s:running_task, a:id) endif call coc#rpc#notify('TaskExit', [a:id, a:code]) endfunction function! s:on_stderr(id, msgs) if get(g:, 'coc_vim_leaving', 0) | return | endif if empty(a:msgs) return endif if s:is_vim call coc#rpc#notify('TaskStderr', [a:id, a:msgs]) else let remain = get(s:err_remain_text, a:id, '') let eof = (a:msgs == ['']) let msgs = copy(a:msgs) if len(remain) > 0 if msgs[0] == '' let msgs[0] = remain else let msgs[0] = remain . msgs[0] endif endif let last = msgs[len(msgs) - 1] let s:err_remain_text[a:id] = len(last) > 0 ? last : '' " all lines from 0 to n - 2 if len(msgs) > 1 call coc#rpc#notify('TaskStderr', [a:id, msgs[:len(msgs)-2]]) elseif eof && len(msgs[0]) > 0 call coc#rpc#notify('TaskStderr', [a:id, msgs]) endif endif endfunction function! s:on_stdout(id, msgs) if empty(a:msgs) return endif if s:is_vim call coc#rpc#notify('TaskStdout', [a:id, a:msgs]) else let remain = get(s:out_remain_text, a:id, '') let eof = (a:msgs == ['']) let msgs = copy(a:msgs) if len(remain) > 0 if msgs[0] == '' let msgs[0] = remain else let msgs[0] = remain . msgs[0] endif endif let last = msgs[len(msgs) - 1] let s:out_remain_text[a:id] = len(last) > 0 ? last : '' " all lines from 0 to n - 2 if len(msgs) > 1 call coc#rpc#notify('TaskStdout', [a:id, msgs[:len(msgs)-2]]) elseif eof && len(msgs[0]) > 0 call coc#rpc#notify('TaskStdout', [a:id, msgs]) endif endif endfunction function! coc#task#running(id) if !has_key(s:running_task, a:id) == 1 return v:false endif let job = s:running_task[a:id] if s:is_vim let status = job_status(job) return status ==# 'run' endif let [code] = jobwait([job], 10) return code == -1 endfunction ================================================ FILE: autoload/coc/terminal.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:channel_map = {} let s:is_win = has('win32') || has('win64') " start terminal, return [bufnr, pid] function! coc#terminal#start(cmd, cwd, env, strict) abort if s:is_vim && !has('terminal') throw 'terminal feature not supported by current vim.' endif let cwd = empty(a:cwd) ? getcwd() : a:cwd execute 'belowright '.get(g:, 'coc_terminal_height', 8).'new +setl\ buftype=nofile' setl winfixheight setl norelativenumber setl nonumber setl bufhidden=hide if exists('&winfixbuf') setl winfixbuf endif if exists('#User#CocTerminalOpen') exe 'doautocmd User CocTerminalOpen' endif let bufnr = bufnr('%') let env = {} let original = {} if !empty(a:env) " use env option when possible if s:is_vim let env = copy(a:env) elseif exists('*setenv') for key in keys(a:env) let original[key] = getenv(key) call setenv(key, a:env[key]) endfor endif endif function! s:OnExit(status) closure call coc#rpc#notify('CocAutocmd', ['TermExit', bufnr, a:status]) if a:status == 0 execute 'silent! bd! '.bufnr endif endfunction if s:is_vim let res = term_start(a:cmd, { \ 'cwd': cwd, \ 'term_kill': s:is_win ? 'kill' : 'term', \ 'term_finish': 'close', \ 'exit_cb': {job, status -> s:OnExit(status)}, \ 'curwin': 1, \ 'env': env, \}) if res == 0 throw 'create terminal job failed' endif let job = term_getjob(bufnr) let s:channel_map[bufnr] = job_getchannel(job) wincmd p return [bufnr, job_info(job).process] else let job_id = termopen(a:cmd, { \ 'cwd': cwd, \ 'pty': v:true, \ 'on_exit': {job, status -> s:OnExit(status)}, \ 'env': env, \ 'clear_env': a:strict ? v:true : v:false \ }) if !empty(original) && exists('*setenv') for key in keys(original) call setenv(key, original[key]) endfor endif if job_id == 0 throw 'create terminal job failed' endif wincmd p let s:channel_map[bufnr] = job_id return [bufnr, jobpid(job_id)] endif endfunction function! coc#terminal#send(bufnr, text, add_new_line) abort let chan = get(s:channel_map, a:bufnr, v:null) if empty(chan) | return| endif if s:is_vim if !a:add_new_line call ch_sendraw(chan, a:text) else call ch_sendraw(chan, a:text.(s:is_win ? "\r\n" : "\n")) endif else let lines = split(a:text, '\v\r?\n') if a:add_new_line && !empty(lines[len(lines) - 1]) if s:is_win call add(lines, "\r\n") else call add(lines, '') endif endif call chansend(chan, lines) let winid = bufwinid(a:bufnr) if winid != -1 call win_execute(winid, 'noa normal! G') endif endif endfunction function! coc#terminal#close(bufnr) abort if !s:is_vim let job_id = get(s:channel_map, a:bufnr, 0) if !empty(job_id) silent! call chanclose(job_id) endif endif exe 'silent! bd! '.a:bufnr endfunction function! coc#terminal#show(bufnr, opts) abort if !bufloaded(a:bufnr) return v:false endif let winids = win_findbuf(a:bufnr) if index(winids, win_getid()) != -1 execute 'normal! G' return v:true endif let curr_winid = -1 for winid in winids if get(get(getwininfo(winid), 0, {}), 'tabnr', 0) == tabpagenr() let curr_winid = winid else call coc#window#close(winid) endif endfor let height = get(a:opts, 'height', 8) if curr_winid == -1 execute 'below '.a:bufnr.'sb' execute 'resize '.height call coc#util#do_autocmd('CocTerminalOpen') else call win_gotoid(curr_winid) endif execute 'normal! G' if get(a:opts, 'preserveFocus', v:false) execute 'wincmd p' endif return v:true endfunction ================================================ FILE: autoload/coc/text.vim ================================================ vim9script export def LinesEqual(one: list, two: list): bool if len(one) != len(two) return false endif for i in range(0, len(one) - 1) if one[i] !=# two[i] return false endif endfor return true enddef # Slice like javascript by character index export def Slice(str: string, start_idx: number, end_idx: any = null): string if end_idx == null return str[start_idx : ] endif if start_idx >= end_idx return '' endif return str[start_idx : end_idx - 1] enddef # Function to check if a string starts with a given prefix export def StartsWith(str: string, prefix: string): bool return str =~# '^' .. prefix enddef # Function to check if a string ends with a given suffix export def EndsWith(str: string, suffix: string): bool return str =~# suffix .. '$' enddef # UTF16 character index in line to byte index. export def Byte_index(line: string, character: number): number if character == 0 return 0 endif var i = 0 var len = 0 for char in split(line, '\zs') i += char2nr(char) > 65535 ? 2 : 1 len += strlen(char) if i >= character break endif endfor return len enddef # Character index of current vim encoding. export def Char_index(line: string, colIdx: number): number return strpart(line, 0, colIdx)->strchars() enddef # Using character indexes export def LcsDiff(str1: string, str2: string): list> def Lcs(a: string, b: string): string var matrix = [] for i in range(0, strchars(a)) matrix[i] = [] for j in range(0, strchars(b)) if i == 0 || j == 0 matrix[i][j] = 0 elseif a[i] == b[j] matrix[i][j] = matrix[i - 1][j - 1] + 1 else matrix[i][j] = max([matrix[i - 1][j], matrix[i][j - 1]]) endif endfor endfor var result = '' var i = strchars(a) - 1 var j = strchars(b) - 1 while i >= 0 && j >= 0 if a[i] == b[j] result = a[i] .. result i -= 1 j -= 1 elseif matrix[i - 1][j] > matrix[i][j - 1] i -= 1 else j -= 1 endif endwhile return result enddef const len1 = strchars(str1) const len2 = strchars(str2) var common = Lcs(str1, str2) var result = [] var i1 = 0 var i2 = 0 var ic = 0 while ic < strchars(common) # 处理str1中不在公共序列的部分 while i1 < len1 && str1[i1] != common[ic] result->add({type: '-', char: str1[i1]}) i1 += 1 endwhile # 处理str2中不在公共序列的部分 while i2 < len2 && str2[i2] != common[ic] result->add({type: '+', char: str2[i2]}) i2 += 1 endwhile # 添加公共字符 if ic < strchars(common) result->add({type: '=', char: common[ic]}) i1 += 1 i2 += 1 ic += 1 endif endwhile # 处理剩余字符 while i1 < len1 result->add({type: '-', char: str1[i1]}) i1 += 1 endwhile while i2 < len2 result->add({type: '+', char: str2[i2]}) i2 += 1 endwhile return result enddef # Get the single changed part, by character index of cursor. def SimpleStringDiff(oldStr: string, newStr: string, charIdx: number = -1): dict var suffixLen = 0 const old_length = strchars(oldStr) const new_length = strchars(newStr) var maxSuffixLen = 0 if charIdx >= 0 maxSuffixLen = min([old_length, new_length - charIdx]) while suffixLen < maxSuffixLen if strcharpart(oldStr, old_length - suffixLen - 1, 1) != strcharpart(newStr, new_length - suffixLen - 1, 1) break endif suffixLen += 1 endwhile else maxSuffixLen = min([old_length, new_length]) while suffixLen < maxSuffixLen if strcharpart(oldStr, old_length - suffixLen - 1, 1) != strcharpart(newStr, new_length - suffixLen - 1, 1) break endif suffixLen += 1 endwhile endif var prefixLen = 0 var remainingLen = min([old_length - suffixLen, new_length - suffixLen]) while prefixLen < remainingLen if strcharpart(oldStr, prefixLen, 1) != strcharpart(newStr, prefixLen, 1) break endif prefixLen += 1 endwhile # Reduce suffixLen if suffixLen == new_length - charIdx const max = min([old_length, new_length]) - prefixLen - suffixLen var i = 0 while i < max if strcharpart(oldStr, old_length - suffixLen - 1, 1) != strcharpart(newStr, new_length - suffixLen - 1, 1) break endif suffixLen += 1 i += 1 endwhile endif const endIndex = old_length - suffixLen echo suffixLen return { oldStart: prefixLen, oldEnd: endIndex, newText: Slice(newStr, prefixLen, new_length - suffixLen), } enddef # Search for new start position of diff in new string export def SearchChangePosition(newStr: string, oldStr: string, diff: dict): number var result = -1 const delta = diff.oldEnd - diff.oldStart const oldText = Slice(oldStr, diff.oldStart, diff.oldEnd) def CheckPosition(idx: number): bool if delta == 0 || Slice(newStr, idx, idx + delta) ==# oldText result = idx return true endif return false enddef if Slice(oldStr, 0, diff.oldStart) ==# Slice(newStr, 0, diff.oldStart) && CheckPosition(diff.oldStart) return result endif const diffs = LcsDiff(oldStr, newStr) # oldStr index var used = 0 # newStr index var index = 0 # Until used reached diff.oldStart var i = 0 for d in diffs if d.type ==# '-' used += 1 elseif d.type ==# '+' index += 1 else used += 1 index += 1 endif if used == diff.oldStart && CheckPosition(index) break endif endfor return result enddef # 0 based start index and end index export def SimpleApplyDiff(text: string, startIdx: number, endIdx: number, insert: string): string return Slice(text, 0, startIdx) .. insert .. Slice(text, endIdx) enddef # Apply change from original to current for newText export def DiffApply(original: string, current: string, newText: string, colIdx: number): any if original ==# current return newText endif const charIdx = colIdx == -1 ? -1 : Char_index(current, colIdx) const diff = SimpleStringDiff(original, current, charIdx) const delta = diff.oldEnd - diff.oldStart const idx = SearchChangePosition(newText, original, diff) if idx == -1 return null endif return SimpleApplyDiff(newText, idx, idx + delta, diff.newText) enddef ================================================ FILE: autoload/coc/ui.vim ================================================ let s:is_vim = !has('nvim') let s:is_win = has('win32') || has('win64') let s:is_mac = has('mac') let s:root = expand(':h:h:h') let s:sign_api = exists('*sign_getplaced') && exists('*sign_place') let s:sign_groups = [] let s:outline_preview_bufnr = 0 let s:is_win32unix = has('win32unix') " Check and function! coc#ui#check_pum_keymappings(trigger) abort if get(g:, 'coc_disable_mappings_check', 0) == 1 return endif if a:trigger !=# 'none' for key in ['', '', '', ''] let arg = maparg(key, 'i', 0, 1) if get(arg, 'expr', 0) let rhs = get(arg, 'rhs', '') if rhs =~# '\"', 'coc#pum#next(1)', '') let rhs = substitute(rhs, '\c"\\"', 'coc#pum#prev(1)', '') let rhs = substitute(rhs, '\c"\\"', 'coc#pum#confirm()', '') execute 'inoremap '.arg['lhs'].' '.rhs endif endif endfor endif endfunction function! coc#ui#quickpick(title, items, cb) abort if exists('*popup_menu') function! s:QuickpickHandler(id, result) closure call a:cb(v:null, a:result) endfunction function! s:QuickpickFilter(id, key) closure for i in range(1, len(a:items)) if a:key == string(i) call popup_close(a:id, i) return 1 endif endfor " No shortcut, pass to generic filter return popup_filter_menu(a:id, a:key) endfunction try call popup_menu(a:items, { \ 'title': a:title, \ 'filter': function('s:QuickpickFilter'), \ 'callback': function('s:QuickpickHandler'), \ }) redraw catch /.*/ call a:cb(v:exception) endtry else let res = inputlist([a:title] + map(range(1, len(a:items)), 'v:val . ". " . a:items[v:val - 1]')) call a:cb(v:null, res) endif endfunction " cmd, cwd function! coc#ui#open_terminal(opts) abort if s:is_vim && !exists('*term_start') echohl WarningMsg | echon "Your vim doesn't have terminal support!" | echohl None return endif if get(a:opts, 'position', 'bottom') ==# 'bottom' let p = '5new' else let p = 'vnew' endif execute 'belowright '.p.' +setl\ buftype=nofile ' setl buftype=nofile setl winfixheight setl norelativenumber setl nonumber setl bufhidden=wipe if exists('#User#CocTerminalOpen') exe 'doautocmd User CocTerminalOpen' endif let cmd = get(a:opts, 'cmd', '') let autoclose = get(a:opts, 'autoclose', 1) if empty(cmd) throw 'command required!' endif let cwd = get(a:opts, 'cwd', getcwd()) let keepfocus = get(a:opts, 'keepfocus', 0) let bufnr = bufnr('%') let Callback = get(a:opts, 'Callback', v:null) function! s:OnExit(status) closure let content = join(getbufline(bufnr, 1, '$'), "\n") if a:status == 0 && autoclose == 1 execute 'silent! bd! '.bufnr endif if !empty(Callback) call call(Callback, [a:status, bufnr, content]) endif endfunction if s:is_vim if s:is_win let cmd = ['cmd.exe', '/C', cmd] endif call term_start(cmd, { \ 'cwd': cwd, \ 'term_finish': 'close', \ 'exit_cb': {job, status -> s:OnExit(status)}, \ 'curwin': 1, \}) else call termopen(cmd, { \ 'cwd': cwd, \ 'on_exit': {job, status -> s:OnExit(status)}, \}) endif if keepfocus wincmd p endif return bufnr endfunction " run command in terminal function! coc#ui#run_terminal(opts, cb) let cmd = get(a:opts, 'cmd', '') if empty(cmd) return a:cb('command required for terminal') endif let opts = { \ 'cmd': cmd, \ 'cwd': empty(get(a:opts, 'cwd', '')) ? getcwd() : a:opts['cwd'], \ 'keepfocus': get(a:opts, 'keepfocus', 0), \ 'Callback': {status, bufnr, content -> a:cb(v:null, {'success': status == 0 ? v:true : v:false, 'bufnr': bufnr, 'content': content})} \} call coc#ui#open_terminal(opts) endfunction function! coc#ui#fix() abort let file = s:root .. '/esbuild.js' if filereadable(file) let opts = { \ 'cmd': 'npm ci', \ 'cwd': s:root, \ 'keepfocus': 1, \ 'Callback': {_ -> execute('CocRestart')} \} call coc#ui#open_terminal(opts) endif endfunction function! coc#ui#echo_hover(msg) echohl MoreMsg echo a:msg echohl None let g:coc_last_hover_message = a:msg endfunction function! coc#ui#echo_messages(hl, msgs) if a:hl !~# 'Error' && (mode() !~# '\v^(i|n)$') return endif let msgs = filter(copy(a:msgs), '!empty(v:val)') if empty(msgs) return endif execute 'echohl '.a:hl echom join(msgs, "\n") echohl None endfunction function! coc#ui#preview_info(lines, filetype, ...) abort pclose keepalt new +setlocal\ previewwindow|setlocal\ buftype=nofile|setlocal\ noswapfile|setlocal\ wrap [Document] setl bufhidden=wipe setl nobuflisted setl nospell exe 'setl filetype='.a:filetype setl conceallevel=0 setl nofoldenable for command in a:000 execute command endfor call append(0, a:lines) exe "normal! z" . len(a:lines) . "\" exe "normal! gg" wincmd p endfunction function! coc#ui#open_files(files) let bufnrs = [] " added on latest vim8 for filepath in a:files let file = fnamemodify(coc#util#node_to_win32unix(filepath), ':.') if bufloaded(file) call add(bufnrs, bufnr(file)) else let bufnr = bufadd(file) call bufload(file) call add(bufnrs, bufnr) call setbufvar(bufnr, '&buflisted', 1) endif endfor doautocmd BufEnter return bufnrs endfunction function! coc#ui#echo_lines(lines) echo join(a:lines, "\n") endfunction function! coc#ui#echo_signatures(signatures) abort if pumvisible() | return | endif echo "" for i in range(len(a:signatures)) call s:echo_signature(a:signatures[i]) if i != len(a:signatures) - 1 echon "\n" endif endfor endfunction function! s:echo_signature(parts) for part in a:parts let hl = get(part, 'type', 'Normal') let text = get(part, 'text', '') if !empty(text) execute 'echohl '.hl execute "echon '".substitute(text, "'", "''", 'g')."'" echohl None endif endfor endfunction function! coc#ui#iterm_open(dir) return s:osascript( \ 'if application "iTerm2" is not running', \ 'error', \ 'end if') && s:osascript( \ 'tell application "iTerm2"', \ 'tell current window', \ 'create tab with default profile', \ 'tell current session', \ 'write text "cd ' . a:dir . '"', \ 'write text "clear"', \ 'activate', \ 'end tell', \ 'end tell', \ 'end tell') endfunction function! s:osascript(...) abort let args = join(map(copy(a:000), '" -e ".shellescape(v:val)'), '') call s:system('osascript'. args) return !v:shell_error endfunction function! s:system(cmd) let output = system(a:cmd) if v:shell_error && output !=# "" echohl Error | echom output | echohl None return endif return output endfunction function! coc#ui#set_lines(bufnr, changedtick, original, replacement, start, end, changes, cursor, col, linecount) abort try if s:is_vim call coc#vim9#Set_lines(a:bufnr, a:changedtick, a:original, a:replacement, a:start, a:end, a:changes, a:cursor, a:col, a:linecount) else call v:lua.require('coc.text').set_lines(a:bufnr, a:changedtick, a:original, a:replacement, a:start, a:end, a:changes, a:cursor, a:col, a:linecount) endif catch /.*/ " Need try catch here on vim9 call coc#compat#send_error('coc#ui#set_lines', s:is_vim) endtry endfunction function! coc#ui#change_lines(bufnr, list) abort if !bufloaded(a:bufnr) return v:null endif undojoin for [lnum, line] in a:list call setbufline(a:bufnr, lnum + 1, line) endfor endfunction function! coc#ui#open_url(url) if isdirectory(a:url) && $TERM_PROGRAM ==# "iTerm.app" call coc#ui#iterm_open(a:url) return endif if !empty(get(g:, 'coc_open_url_command', '')) call system(g:coc_open_url_command.' '.a:url) return endif if has('mac') && executable('open') call system('open "'.a:url.'"') return endif if executable('xdg-open') call system('xdg-open "'.a:url.'"') return endif call system('cmd /c start "" /b '. substitute(a:url, '&', '^&', 'g')) if v:shell_error echohl Error | echom 'Failed to open '.a:url | echohl None return endif endfunction function! coc#ui#rename_file(oldPath, newPath, write) abort let oldPath = coc#util#node_to_win32unix(a:oldPath) let newPath = coc#util#node_to_win32unix(a:newPath) let bufnr = bufnr(oldPath) if bufnr == -1 throw 'Unable to get bufnr of '.oldPath endif if oldPath =~? newPath && (s:is_mac || s:is_win || s:is_win32unix) return coc#ui#safe_rename(bufnr, oldPath, newPath, a:write) endif if bufloaded(newPath) execute 'silent bdelete! '.bufnr(newPath) endif " TODO use nvim_buf_set_name instead let current = bufnr == bufnr('%') let bufname = fnamemodify(newPath, ":~:.") let filepath = fnamemodify(bufname(bufnr), '%:p') let winid = coc#compat#buf_win_id(bufnr) let curr = -1 if winid == -1 let curr = win_getid() let file = fnamemodify(bufname(bufnr), ':.') execute 'keepalt tab drop '.fnameescape(bufname(bufnr)) let winid = win_getid() endif call win_execute(winid, 'keepalt file '.fnameescape(bufname), 'silent') call win_execute(winid, 'doautocmd BufEnter') if a:write call win_execute(winid, 'noa write!', 'silent') call delete(filepath, '') endif if curr != -1 call win_gotoid(curr) endif return bufnr endfunction " System is case in sensitive and newPath have different case. function! coc#ui#safe_rename(bufnr, oldPath, newPath, write) abort let winid = win_getid() let lines = getbufline(a:bufnr, 1, '$') execute 'keepalt tab drop '.fnameescape(fnamemodify(a:oldPath, ':.')) let view = winsaveview() execute 'keepalt bwipeout! '.a:bufnr if a:write call delete(a:oldPath, '') endif execute 'keepalt edit '.fnameescape(fnamemodify(a:newPath, ':~:.')) let bufnr = bufnr('%') call coc#compat#buf_set_lines(bufnr, 0, -1, lines) if a:write execute 'noa write' endif call winrestview(view) call win_gotoid(winid) return bufnr endfunction function! coc#ui#sign_unplace() abort if exists('*sign_unplace') for group in s:sign_groups call sign_unplace(group) endfor endif endfunction function! coc#ui#update_signs(bufnr, group, signs) abort if !s:sign_api || !bufloaded(a:bufnr) return endif call sign_unplace(a:group, {'buffer': a:bufnr}) for def in a:signs let opts = {'lnum': def['lnum']} if has_key(def, 'priority') let opts['priority'] = def['priority'] endif call sign_place(0, a:group, def['name'], a:bufnr, opts) endfor endfunction function! coc#ui#outline_preview(config) abort let view_id = get(w:, 'cocViewId', '') if view_id !=# 'OUTLINE' return endif let wininfo = get(getwininfo(win_getid()), 0, v:null) if empty(wininfo) return endif let border = get(a:config, 'border', v:true) let th = &lines - &cmdheight - 2 let range = a:config['range'] let height = min([range['end']['line'] - range['start']['line'] + 1, th - 4]) let to_left = &columns - wininfo['wincol'] - wininfo['width'] < wininfo['wincol'] let start_lnum = range['start']['line'] + 1 let end_lnum = range['end']['line'] + 1 - start_lnum > &lines ? start_lnum + &lines : range['end']['line'] + 1 let lines = getbufline(a:config['bufnr'], start_lnum, end_lnum) let content_width = max(map(copy(lines), 'strdisplaywidth(v:val)')) let width = min([content_width, a:config['maxWidth'], to_left ? wininfo['wincol'] - 3 : &columns - wininfo['wincol'] - wininfo['width']]) let filetype = getbufvar(a:config['bufnr'], '&filetype') let cursor_row = coc#cursor#screen_pos()[0] let config = { \ 'relative': 'editor', \ 'row': cursor_row - 1 + height < th ? cursor_row - (border ? 1 : 0) : th - height - (border ? 1 : -1), \ 'col': to_left ? wininfo['wincol'] - 4 - width : wininfo['wincol'] + wininfo['width'], \ 'width': width, \ 'height': height, \ 'lines': lines, \ 'border': border ? [1,1,1,1] : v:null, \ 'rounded': get(a:config, 'rounded', 1) ? 1 : 0, \ 'winblend': a:config['winblend'], \ 'highlight': a:config['highlight'], \ 'borderhighlight': a:config['borderhighlight'], \ } let winid = coc#float#get_float_by_kind('outline-preview') let result = coc#float#create_float_win(winid, s:outline_preview_bufnr, config) if empty(result) return v:null endif call setwinvar(result[0], 'kind', 'outline-preview') let s:outline_preview_bufnr = result[1] if !empty(filetype) call win_execute(result[0], 'setfiletype '.filetype) endif return result[1] endfunction function! coc#ui#outline_close_preview() abort let winid = coc#float#get_float_by_kind('outline-preview') if winid call coc#float#close(winid) endif endfunction " Ignore error from autocmd when file opened function! coc#ui#safe_open(cmd, file) abort let bufname = fnameescape(a:file) try execute 'silent! '. a:cmd.' '.bufname catch /.*/ if bufname('%') != bufname throw 'Error on open '. v:exception endif endtry endfunction " Use noa to setloclist, avoid BufWinEnter autocmd function! coc#ui#setloclist(nr, items, action, title) abort let items = s:is_win32unix ? map(copy(a:items), 's:convert_qfitem(v:val)'): a:items if a:action ==# ' ' let title = get(getloclist(a:nr, {'title': 1}), 'title', '') let action = title ==# a:title ? 'r' : ' ' noa call setloclist(a:nr, [], action, {'title': a:title, 'items': items}) else noa call setloclist(a:nr, [], a:action, {'title': a:title, 'items': items}) endif endfunction function! s:convert_qfitem(item) abort let result = copy(a:item) if has_key(result, 'filename') let result['filename'] = coc#util#node_to_win32unix(result['filename']) endif return result endfunction function! coc#ui#get_mouse() abort if get(g:, 'coc_node_env', '') ==# 'test' return get(g:, 'mouse_position', [win_getid(), line('.'), col('.')]) endif return [v:mouse_winid,v:mouse_lnum,v:mouse_col] endfunction " viewId - identifier of tree view " bufnr - bufnr tree view " winid - winid of tree view " bufname - bufname of tree view " command - split command " optional options - bufhidden, canSelectMany, winfixwidth function! coc#ui#create_tree(opts) abort let viewId = a:opts['viewId'] let bufname = a:opts['bufname'] let tabid = coc#compat#tabnr_id(tabpagenr()) let winid = s:get_tree_winid(a:opts) let bufnr = a:opts['bufnr'] if !bufloaded(bufnr) let bufnr = -1 endif if winid != -1 call win_gotoid(winid) if bufnr('%') == bufnr return [bufnr, winid, tabid] elseif bufnr != -1 execute 'silent keepalt buffer '.bufnr else execute 'silent keepalt edit +setl\ buftype=nofile '.bufname call s:set_tree_defaults(a:opts) endif else " need to split let cmd = get(a:opts, 'command', 'belowright 30vs') execute 'silent keepalt '.cmd.' +setl\ buftype=nofile '.bufname call s:set_tree_defaults(a:opts) let winid = win_getid() endif let w:cocViewId = viewId return [winbufnr(winid), winid, tabid] endfunction " valid window id or -1 function! s:get_tree_winid(opts) abort let viewId = a:opts['viewId'] let winid = a:opts['winid'] if winid != -1 && coc#window#visible(winid) return winid endif if winid != -1 call win_execute(winid, 'noa close!', 'silent!') endif return coc#window#find('cocViewId', viewId) endfunction function! s:set_tree_defaults(opts) abort let bufhidden = get(a:opts, 'bufhidden', 'wipe') let signcolumn = get(a:opts, 'canSelectMany', v:false) ? 'yes' : 'no' let winfixwidth = get(a:opts, 'winfixwidth', v:false) ? ' winfixwidth' : '' execute 'setl bufhidden='.bufhidden.' signcolumn='.signcolumn.winfixwidth setl nolist nonumber norelativenumber foldcolumn=0 setl nocursorline nobuflisted wrap undolevels=-1 filetype=coctree nomodifiable noswapfile endfunction ================================================ FILE: autoload/coc/util.vim ================================================ scriptencoding utf-8 let s:root = expand(':h:h:h') let s:is_win = has('win32') || has('win64') let s:is_vim = !has('nvim') let s:vim_api_version = 38 let s:is_win32unix = has('win32unix') let s:win32unix_prefix = '' let s:win32unix_fix_home = 0 if s:is_win32unix let home = expand('$HOME') if strpart(home, 0, 6) ==# '/home/' let s:win32unix_fix_home = 1 let s:win32unix_prefix = '/' elseif strpart(home, 0, 3) =~# '^/\w/' let s:win32unix_prefix = '/' else let s:win32unix_prefix = matchstr(home, '^\/\w\+\/') endif endif let s:win32unix_prefix_len = strlen(s:win32unix_prefix) function! coc#util#merge_winhl(curr, hls) abort let highlightMap = {} for parts in map(split(a:curr, ','), 'split(v:val, ":")') if len(parts) == 2 let highlightMap[parts[0]] = parts[1] endif endfor for item in a:hls let highlightMap[item[0]] = item[1] endfor return join(map(items(highlightMap), 'v:val[0].":".v:val[1]'), ',') endfunction function! coc#util#api_version() abort return s:vim_api_version endfunction function! coc#util#semantic_hlgroups() abort let res = split(execute('hi'), "\n") let filtered = filter(res, "v:val =~# '^CocSem' && v:val !~# ' cleared$'") return map(filtered, "matchstr(v:val,'\\v^CocSem\\w+')") endfunction " get cursor position function! coc#util#cursor() return [line('.') - 1, coc#string#character_length(strpart(getline('.'), 0, col('.') - 1))] endfunction function! coc#util#change_info() abort return {'lnum': line('.'), 'col': col('.'), 'line': getline('.'), 'changedtick': b:changedtick} endfunction function! coc#util#jumpTo(line, character) abort echohl WarningMsg | echon 'coc#util#jumpTo is deprecated, use coc#cursor#move_to instead.' | echohl None call coc#cursor#move_to(a:line, a:character) endfunction function! coc#util#root_patterns() abort return coc#rpc#request('rootPatterns', [bufnr('%')]) endfunction function! coc#util#get_config(key) abort return coc#rpc#request('getConfig', [a:key]) endfunction function! coc#util#open_terminal(opts) abort return coc#ui#open_terminal(a:opts) endfunction function! coc#util#synname() abort return synIDattr(synID(line('.'), col('.') - 1, 1), 'name') endfunction function! coc#util#version() if s:is_vim return string(v:versionlong) endif let c = execute('silent version') let lines = split(matchstr(c, 'NVIM v\zs[^\n-]*')) return lines[0] endfunction function! coc#util#check_refresh(bufnr) if !bufloaded(a:bufnr) return 0 endif if getbufvar(a:bufnr, 'coc_diagnostic_disable', 0) return 0 endif return 1 endfunction function! coc#util#diagnostic_info(bufnr, checkInsert) abort let checked = coc#util#check_refresh(a:bufnr) if !checked return v:null endif if a:checkInsert && mode() =~# '^i' return v:null endif let locationlist = '' let winid = -1 for info in getwininfo() if info['bufnr'] == a:bufnr let winid = info['winid'] let locationlist = get(getloclist(winid, {'title': 1}), 'title', '') break endif endfor return { \ 'bufnr': bufnr('%'), \ 'winid': winid, \ 'lnum': winid == -1 ? -1 : coc#window#get_cursor(winid)[0], \ 'locationlist': locationlist \ } endfunction function! coc#util#job_command() if (has_key(g:, 'coc_node_path')) let node = expand(g:coc_node_path) else let node = $COC_NODE_PATH == '' ? 'node' : $COC_NODE_PATH endif if !executable(node) echohl Error | echom '[coc.nvim] "'.node.'" is not executable, checkout https://nodejs.org/en/download/' | echohl None return endif if !filereadable(s:root.'/build/index.js') if isdirectory(s:root.'/src') echohl Error | echom '[coc.nvim] build/index.js not found, please install dependencies and compile coc.nvim by: npm ci' | echohl None else echohl Error | echon '[coc.nvim] your coc.nvim is broken.' | echohl None endif return endif let default = ['--no-warnings'] return [node] + get(g:, 'coc_node_args', default) + [s:root.'/build/index.js'] endfunction function! coc#util#open_file(cmd, file) let file = coc#util#node_to_win32unix(a:file) execute a:cmd .' '.fnameescape(fnamemodify(file, ':~:.')) return bufnr('%') endfunction function! coc#util#jump(cmd, filepath, ...) abort if a:cmd != 'pedit' silent! normal! m' endif let path = coc#util#node_to_win32unix(a:filepath) let file = fnamemodify(path, ":~:.") if a:cmd ==# 'pedit' let extra = empty(get(a:, 1, [])) ? '' : '+'.(a:1[0] + 1) exe 'pedit '.extra.' '.fnameescape(file) return elseif a:cmd ==# 'drop' let dstbuf = bufadd(path) if bufnr('%') != dstbuf let binfo = getbufinfo(dstbuf) if len(binfo) == 1 && empty(binfo[0].windows) execute 'buffer '.dstbuf let &buflisted = 1 else let saved = &wildignore set wildignore= execute 'drop '.fnameescape(file) execute 'set wildignore='.saved endif endif elseif a:cmd ==# 'edit' && bufloaded(file) exe 'b '.bufnr(file) else call s:safer_open(a:cmd, file) endif if !empty(get(a:, 1, [])) let line = getline(a:1[0] + 1) let col = coc#string#byte_index(line, a:1[1]) + 1 call cursor(a:1[0] + 1, col) endif if &filetype ==# '' filetype detect endif if s:is_vim redraw endif endfunction function! s:safer_open(cmd, file) abort " How to support :pedit and :drop? let is_supported_cmd = index(["edit", "split", "vsplit", "tabe"], a:cmd) >= 0 " Use special handling only for URI. let looks_like_uri = match(a:file, "^.*://") >= 0 if looks_like_uri && is_supported_cmd && has('win32') && exists('*bufadd') " Workaround a bug for Win32 paths. " " reference: " - https://github.com/vim/vim/issues/541 " - https://github.com/neoclide/coc-java/issues/82 " - https://github.com/vim-jp/issues/issues/6 let buf = bufadd(a:file) if a:cmd != 'edit' " Open split, tab, etc. by a:cmd. execute a:cmd endif " Set current buffer to the file exe 'keepjumps buffer ' . buf else if a:cmd =~# 'drop' if a:cmd ==# 'tab drop' && bufexists(a:file) let bufnr = bufnr(a:file) if bufnr == bufnr('%') return endif let winid = coc#window#buf_winid(bufnr) if winid != -1 call win_gotoid(winid) return endif endif let saved = &wildignore set wildignore= let l:old_page_idx = tabpagenr() let l:old_page_cnt = tabpagenr('$') execute 'noautocmd '.a:cmd.' '.fnameescape(a:file) if tabpagenr('$') > l:old_page_cnt doautocmd TabNew doautocmd BufNew doautocmd BufAdd endif let l:new_page_idx = tabpagenr() if l:new_page_idx != l:old_page_idx exec 'noautocmd tabnext '.l:old_page_idx doautocmd TabLeave doautocmd BufLeave exec 'noautocmd tabnext '.l:new_page_idx endif doautocmd TabEnter doautocmd BufReadPre doautocmd BufReadPost doautocmd BufEnter if l:new_page_idx != l:old_page_idx doautocmd BufWinEnter endif execute 'set wildignore='.saved else execute a:cmd.' '.fnameescape(a:file) endif endif endfunction function! coc#util#variables(bufnr) abort let info = getbufinfo(a:bufnr) let variables = empty(info) ? {} : copy(info[0]['variables']) for key in keys(variables) if key !~# '\v^coc' unlet variables[key] endif endfor return variables endfunction function! coc#util#with_callback(method, args, cb) function! s:Cb() closure try let res = call(a:method, a:args) call a:cb(v:null, res) catch /.*/ call a:cb(v:exception) endtry endfunction let timeout = s:is_vim ? 10 : 0 call timer_start(timeout, {-> s:Cb() }) endfunction function! coc#util#timer(method, args) call timer_start(0, { -> s:Call(a:method, a:args)}) endfunction function! s:Call(method, args) try call call(a:method, a:args) " don't redraw for command-line/prompt mode if mode() !~# '^[cr]' redraw endif catch /.*/ return 0 endtry endfunction " Global vim information function! coc#util#vim_info() return { \ 'root': coc#util#win32unix_to_node(s:root), \ 'apiversion': s:vim_api_version, \ 'mode': mode(), \ 'config': get(g:, 'coc_user_config', {}), \ 'floating': !s:is_vim && exists('*nvim_open_win') ? v:true : v:false, \ 'extensionRoot': coc#util#extension_root(), \ 'globalExtensions': get(g:, 'coc_global_extensions', []), \ 'lines': &lines, \ 'columns': &columns, \ 'cmdheight': &cmdheight, \ 'pid': coc#util#getpid(), \ 'filetypeMap': get(g:, 'coc_filetype_map', {}), \ 'version': coc#util#version(), \ 'pumevent': 1, \ 'dialog': !s:is_vim || has('popupwin') ? v:true : v:false, \ 'terminal': !s:is_vim || has('terminal') ? v:true : v:false, \ 'unixPrefix': s:win32unix_prefix, \ 'jumpAutocmd': coc#util#check_jump_autocmd(), \ 'isVim': s:is_vim ? v:true : v:false, \ 'isCygwin': s:is_win32unix ? v:true : v:false, \ 'isMacvim': has('gui_macvim') ? v:true : v:false, \ 'isiTerm': $TERM_PROGRAM ==# "iTerm.app", \ 'colorscheme': get(g:, 'colors_name', ''), \ 'workspaceFolders': get(g:, 'WorkspaceFolders', v:null), \ 'background': &background, \ 'runtimepath': join(coc#compat#list_runtime_paths(), ','), \ 'locationlist': get(g:,'coc_enable_locationlist', 1), \ 'progpath': v:progpath, \ 'guicursor': &guicursor, \ 'pumwidth': exists('&pumwidth') ? &pumwidth : 15, \ 'tabCount': tabpagenr('$'), \ 'vimCommands': get(g:, 'coc_vim_commands', []), \ 'virtualText': v:true, \ 'sign': exists('*sign_place') && exists('*sign_unplace'), \ 'ambiguousIsNarrow': &ambiwidth ==# 'single' ? v:true : v:false, \ 'textprop': has('textprop') ? v:true : v:false, \ 'semanticHighlights': coc#util#semantic_hlgroups() \} endfunction function! coc#util#check_jump_autocmd() abort let autocmd_event = 'User' let autocmd_group = 'CocJumpPlaceholder' if exists('#' . autocmd_event . '#' . autocmd_group) let content = execute('autocmd ' . autocmd_event . ' ' . autocmd_group) if content =~# 'showSignatureHelp' return v:true endif endif return v:false endfunction function! coc#util#all_state() return { \ 'bufnr': bufnr('%'), \ 'winid': win_getid(), \ 'bufnrs': map(getbufinfo({'bufloaded': 1}),'v:val["bufnr"]'), \ 'winids': map(getwininfo(),'v:val["winid"]'), \ } endfunction function! coc#util#install() abort call coc#ui#open_terminal({ \ 'cwd': s:root, \ 'cmd': 'npm ci', \ 'autoclose': 0, \ }) endfunction function! coc#util#extension_root() abort return coc#util#get_data_home().'/extensions' endfunction function! coc#util#update_extensions(...) abort let async = get(a:, 1, 0) if async call coc#rpc#notify('updateExtensions', []) else call coc#rpc#request('updateExtensions', [v:true]) endif endfunction function! coc#util#install_extension(args) abort let names = filter(copy(a:args), 'v:val !~# "^-"') let isRequest = index(a:args, '-sync') != -1 if isRequest call coc#rpc#request('installExtensions', names) else call coc#rpc#notify('installExtensions', names) endif endfunction function! coc#util#do_autocmd(name) abort if exists('#User#'.a:name) exe 'doautocmd User '.a:name endif endfunction function! coc#util#refactor_foldlevel(lnum) abort if a:lnum <= 2 | return 0 | endif let line = getline(a:lnum) if line =~# '^\%u3000\s*$' | return 0 | endif return 1 endfunction function! coc#util#refactor_fold_text(lnum) abort let range = '' let info = get(b:line_infos, a:lnum, []) if !empty(info) let range = info[0].':'.info[1] endif return trim(getline(a:lnum)[3:]).' '.range endfunction " get tabsize & expandtab option function! coc#util#get_format_opts(bufnr) abort let bufnr = a:bufnr && bufloaded(a:bufnr) ? a:bufnr : bufnr('%') let tabsize = getbufvar(bufnr, '&shiftwidth') if tabsize == 0 let tabsize = getbufvar(bufnr, '&tabstop') endif return { \ 'tabsize': tabsize, \ 'expandtab': getbufvar(bufnr, '&expandtab'), \ 'insertFinalNewline': getbufvar(bufnr, '&eol'), \ 'trimTrailingWhitespace': getbufvar(bufnr, 'coc_trim_trailing_whitespace', 0), \ 'trimFinalNewlines': getbufvar(bufnr, 'coc_trim_final_newlines', 0) \ } endfunction function! coc#util#get_editoroption(winid) abort let info = get(getwininfo(a:winid), 0, v:null) if empty(info) || coc#window#is_float(a:winid) return v:null endif let bufnr = info['bufnr'] let buftype = getbufvar(bufnr, '&buftype') " avoid window for other purpose. if buftype !=# '' && buftype !=# 'acwrite' return v:null endif return { \ 'bufnr': bufnr, \ 'winid': a:winid, \ 'tabpageid': coc#compat#tabnr_id(info['tabnr']), \ 'winnr': winnr(), \ 'visibleRanges': s:visible_ranges(a:winid), \ 'formatOptions': coc#util#get_format_opts(bufnr), \ } endfunction function! coc#util#get_loaded_bufs() abort return map(getbufinfo({'bufloaded': 1}),'v:val["bufnr"]') endfunction function! coc#util#editor_infos() abort let result = [] for info in getwininfo() if !coc#window#is_float(info['winid']) let bufnr = info['bufnr'] let buftype = getbufvar(bufnr, '&buftype') if buftype !=# '' && buftype !=# 'acwrite' continue endif call add(result, { \ 'winid': info['winid'], \ 'bufnr': bufnr, \ 'tabid': coc#compat#tabnr_id(info['tabnr']), \ 'fullpath': coc#util#get_fullpath(bufnr), \ }) endif endfor return result endfunction function! coc#util#getpid() if !s:is_win32unix return getpid() endif let cmd = 'cat /proc/' . getpid() . '/winpid' return substitute(system(cmd), '\v\n', '', 'gi') endfunction function! coc#util#get_bufoptions(bufnr, max) abort if !bufloaded(a:bufnr) return v:null endif let bufname = bufname(a:bufnr) let buftype = getbufvar(a:bufnr, '&buftype') let commandline = get(getbufinfo(a:bufnr)[0], 'command', 0) || bufname(a:bufnr) == '[Command Line]' let size = coc#util#bufsize(a:bufnr) let lines = v:null if getbufvar(a:bufnr, 'coc_enabled', 1) \ && (buftype == '' || buftype == 'acwrite' || getbufvar(a:bufnr, 'coc_force_attach', 0)) \ && size != -2 \ && size < a:max let lines = getbufline(a:bufnr, 1, '$') endif return { \ 'bufnr': a:bufnr, \ 'commandline': commandline, \ 'size': size, \ 'lines': lines, \ 'winid': bufwinid(a:bufnr), \ 'winids': win_findbuf(a:bufnr), \ 'bufname': bufname, \ 'buftype': buftype, \ 'previewwindow': v:false, \ 'eol': getbufvar(a:bufnr, '&eol'), \ 'variables': coc#util#variables(a:bufnr), \ 'filetype': getbufvar(a:bufnr, '&filetype'), \ 'lisp': getbufvar(a:bufnr, '&lisp'), \ 'iskeyword': getbufvar(a:bufnr, '&iskeyword'), \ 'changedtick': getbufvar(a:bufnr, 'changedtick'), \ 'fullpath': coc#util#get_fullpath(a:bufnr) \} endfunction " Get fullpath for NodeJs of current buffer or bufnr function! coc#util#get_fullpath(...) abort let nr = a:0 == 0 ? bufnr('%') : a:1 if !bufloaded(nr) return '' endif if s:is_vim && getbufvar(nr, '&buftype') ==# 'terminal' let job = term_getjob(nr) let pid = job_info(job)->get('process', 0) let cwd = fnamemodify(getcwd(), ':~') return 'term://' . cwd . '//' . pid . ':' . substitute(bufname(nr), '^!', '', '') endif let name = bufname(nr) return empty(name) ? '' : coc#util#win32unix_to_node(fnamemodify(name, ':p')) endfunction function! coc#util#bufsize(bufnr) abort if bufnr('%') == a:bufnr return line2byte(line("$") + 1) endif let bufname = bufname(a:bufnr) if !getbufvar(a:bufnr, '&modified') && filereadable(bufname) return getfsize(bufname) endif return strlen(join(getbufline(a:bufnr, 1, '$'), '\n')) endfunction function! coc#util#get_config_home(...) let skip_convert = get(a:, 1, 0) let dir = '' if !empty(get(g:, 'coc_config_home', '')) let dir = resolve(expand(g:coc_config_home)) else if exists('$VIMCONFIG') let dir = resolve($VIMCONFIG) else if s:is_vim if s:is_win || s:is_win32unix let dir = s:resolve($HOME, "vimfiles") else if isdirectory(s:resolve($HOME, '.vim')) let dir = s:resolve($HOME, '.vim') else if exists('$XDG_CONFIG_HOME') && isdirectory(resolve($XDG_CONFIG_HOME)) let dir = s:resolve($XDG_CONFIG_HOME, 'vim') else let dir = s:resolve($HOME, '.config/vim') endif endif endif else let appname = empty($NVIM_APPNAME) ? 'nvim' : $NVIM_APPNAME if exists('$XDG_CONFIG_HOME') let dir = s:resolve($XDG_CONFIG_HOME, appname) else if s:is_win let dir = s:resolve($HOME, 'AppData/Local/'.appname) else let dir = s:resolve($HOME, '.config/'.appname) endif endif endif endif endif return skip_convert ? coc#util#fix_home(dir) : coc#util#win32unix_to_node(dir) endfunction function! coc#util#get_data_home() if get(g:, 'coc_node_env', '') ==# 'test' && !empty($COC_DATA_HOME) return coc#util#win32unix_to_node($COC_DATA_HOME) endif if !empty(get(g:, 'coc_data_home', '')) let dir = resolve(expand(g:coc_data_home)) else if exists('$XDG_CONFIG_HOME') && isdirectory(resolve($XDG_CONFIG_HOME)) let dir = s:resolve($XDG_CONFIG_HOME, 'coc') else if s:is_win || s:is_win32unix let dir = resolve(expand('~/AppData/Local/coc')) else let dir = resolve(expand('~/.config/coc')) endif endif endif let dir = coc#util#fix_home(dir) if !isdirectory(dir) call coc#notify#create(['creating coc.nvim data directory: '.dir], { \ 'borderhighlight': 'CocInfoSign', \ 'timeout': 5000, \ 'kind': 'info', \ }) call mkdir(dir, "p", 0755) endif return coc#util#win32unix_to_node(dir) endfunction " Get the fixed home dir on mysys2, use user's home " /home/YourName => /c/User/YourName function! coc#util#fix_home(filepath) abort if s:win32unix_fix_home && strpart(a:filepath, 0, 6) ==# '/home/' return substitute(a:filepath, '^/home', '/c/User', '') endif return a:filepath endfunction " /cygdrive/c/Users/YourName " /mnt/c/Users/YourName " /c/Users/YourName function! coc#util#win32unix_to_node(filepath) abort if s:is_win32unix let fullpath = coc#util#fix_home(a:filepath) if strpart(fullpath, 0, s:win32unix_prefix_len) ==# s:win32unix_prefix let part = strpart(fullpath, s:win32unix_prefix_len) return toupper(part[0]) . ':' . substitute(part[1:], '/', '\', 'g') endif endif return a:filepath endfunction function! coc#util#node_to_win32unix(filepath) abort if s:is_win32unix && a:filepath =~# '^\w:\\' let part = tolower(a:filepath[0]) . a:filepath[2:] return s:win32unix_prefix . substitute(part, '\\', '/', 'g') endif return a:filepath endfunction function! coc#util#get_complete_option() let pos = getcurpos() let line = getline(pos[1]) let input = matchstr(strpart(line, 0, pos[2] - 1), '\k*$') let col = pos[2] - strlen(input) let position = { \ 'line': line('.')-1, \ 'character': coc#string#character_length(strpart(getline('.'), 0, col('.') - 1)) \ } let word = matchstr(strpart(line, col - 1), '^\k\+') let followWord = len(word) > 0 ? strcharpart(word, strchars(input)) : '' return { \ 'word': word, \ 'followWord': followWord, \ 'position': position, \ 'input': empty(input) ? '' : input, \ 'line': line, \ 'filetype': &filetype, \ 'filepath': expand('%:p'), \ 'bufnr': bufnr('%'), \ 'linenr': pos[1], \ 'colnr' : pos[2], \ 'col': col - 1, \ 'changedtick': b:changedtick, \} endfunction function! coc#util#get_changedtick(bufnr) abort if s:is_vim && bufloaded(a:bufnr) call listener_flush(a:bufnr) endif return getbufvar(a:bufnr, 'changedtick') endfunction " Get the valid position from line, character of current buffer function! coc#util#valid_position(line, character) abort let total = line('$') - 1 if a:line > total return [total, 0] endif let max = max([0, coc#string#character_length(getline(a:line + 1)) - (mode() ==# 'n' ? 1 : 0)]) return a:character > max ? [a:line, max] : [a:line, a:character] endfunction function! s:visible_ranges(winid) abort let info = getwininfo(a:winid)[0] let res = [] if !has_key(info, 'topline') || !has_key(info, 'botline') return res endif let begin = 0 let curr = info['topline'] let max = info['botline'] if win_getid() != a:winid return [[curr, max]] endif while curr <= max let closedend = foldclosedend(curr) if closedend == -1 let begin = begin == 0 ? curr : begin if curr == max call add(res, [begin, curr]) endif let curr = curr + 1 else if begin != 0 call add(res, [begin, curr - 1]) let begin = closedend + 1 endif let curr = closedend + 1 endif endwhile return res endfunction " for avoid bug with vim&neovim https://github.com/neoclide/coc.nvim/discussions/5287 function! s:resolve(path, part) abort return resolve(a:path . '/' . a:part) endfunction ================================================ FILE: autoload/coc/vim9.vim ================================================ vim9script scriptencoding utf-8 const default_priority = 1024 const priorities = { 'CocListSearch': 2048, 'CocSearch': 2048, } const diagnostic_hlgroups = ['CocUnusedHighlight', 'CocDeprecatedHighlight', 'CocHintHighlight', 'CocInfoHighlight', 'CocWarningHighlight', 'CocErrorHighlight'] const maxCount = get(g:, 'coc_highlight_maximum_count', 500) const maxTimePerBatchMs = 16 var maxEditCount = get(g:, 'coc_edits_maximum_count', 200) var saved_event_ignore: string = '' def Is_timeout(start_time: list, max: number): bool return (start_time->reltime()->reltimefloat()) * 1000 > max enddef # Some hlGroups have higher priorities. def Get_priority(hlGroup: string, priority: any): number if has_key(priorities, hlGroup) return priorities[hlGroup] endif if type(priority) != v:t_number return default_priority endif const idx = index(diagnostic_hlgroups, hlGroup) if idx != -1 return priority + idx endif return priority enddef def Convert_item(item: any): list if type(item) == v:t_list return item endif # hlGroup, lnum, colStart, colEnd, combine, start_incl, end_incl const combine = has_key(priorities, item.hlGroup) ? 1 : get(item, 'combine', 0) const start_incl = get(item, 'start_incl', 0) const end_incl = get(item, 'end_incl', 0) return [item.hlGroup, item.lnum, item.colStart, item.colEnd, combine, start_incl, end_incl] enddef # Check if lines synchronized as expected export def Check_sha256(bufnr: number, expected: string): bool if !exists('*sha256') return true endif return getbufline(bufnr, 1, '$')->join("\n")->sha256() ==# expected enddef def Create_namespace(key: any): number if type(key) == v:t_number if key == -1 return coc#api#Create_namespace('anonymous') endif return key endif if type(key) != v:t_string throw 'Expect number or string for namespace key.' endif return coc#api#Create_namespace($'coc-{key}') enddef export def Clear_highlights(id: number, key: any, start_line: number = 0, end_line: number = -1): void const buf = id == 0 ? bufnr('%') : id if bufloaded(buf) const ns = Create_namespace(key) coc#api#Buf_clear_namespace(buf, ns, start_line, end_line) endif enddef export def Add_highlight(id: number, key: any, hl_group: string, line: number, col_start: number, col_end: number, opts: dict = {}): void const buf = id == 0 ? bufnr('%') : id if bufloaded(buf) const ns = Create_namespace(key) coc#api#Buf_add_highlight1(buf, ns, hl_group, line, col_start, col_end, opts) endif enddef export def Clear_all(): void const namespaces = coc#api#Get_namespaces() const bufnrs = getbufinfo({'bufloaded': 1})->mapnew((_, o: dict): number => o.bufnr) for ns in values(namespaces) for bufnr in bufnrs coc#api#Buf_clear_namespace(bufnr, ns, 0, -1) endfor endfor enddef # From `coc#highlight#set(`: # type HighlightItem = [hlGroup, lnum, colStart, colEnd, combine?, start_incl?, end_incl?] # From `src/core/highlights.ts`: # type HighlightItemDef = [string, number, number, number, number?, number?, number?] # type HighlightItem = list # type HighlightItemList = list # NOTE: Can't use type on vim9.0.0438 export def Set_highlights(bufnr: number, key: any, highlights: list, priority: any = null): void if bufloaded(bufnr) const changedtick = getbufvar(bufnr, 'changedtick', 0) const ns = Create_namespace(key) Add_highlights_timer(bufnr, ns, highlights, priority, changedtick) endif enddef def Add_highlights_timer(bufnr: number, ns: number, highlights: list, priority: any, changedtick: number): void if !bufloaded(bufnr) || getbufvar(bufnr, 'changedtick', 0) != changedtick return endif const total = len(highlights) const start_time = reltime() var end_idx = 0 for i in range(0, total - 1, maxCount) end_idx = i + maxCount - 1 const hls = highlights[i : end_idx] Add_highlights(bufnr, ns, hls, priority) if Is_timeout(start_time, maxTimePerBatchMs) break endif endfor if end_idx < total - 1 const next = highlights[end_idx + 1 : ] timer_start(10, (_) => Add_highlights_timer(bufnr, ns, next, priority, changedtick)) endif enddef def Add_highlights(bufnr: number, ns: number, highlights: list, priority: any): void final types = coc#api#GetNamespaceTypes(ns)->copy() for highlightItem in highlights const item = Convert_item(highlightItem) var [ hlGroup: string, lnum: number, colStart: number, colEnd: number; _ ] = item if colEnd == -1 colEnd = getbufline(bufnr, lnum + 1)->get(0, '')->strlen() endif const type: string = $'{hlGroup}_{ns}' if index(types, type) == -1 const opts: dict = { 'priority': Get_priority(hlGroup, priority), 'hl_mode': get(item, 4, 1) ? 'combine' : 'override', 'start_incl': get(item, 5, 0), 'end_incl': get(item, 6, 0), } coc#api#CreateType(ns, hlGroup, opts) add(types, type) endif const propId: number = coc#api#GeneratePropId(bufnr) try prop_add(lnum + 1, colStart + 1, {'bufnr': bufnr, 'type': type, 'id': propId, 'end_col': colEnd + 1}) catch /^Vim\%((\a\+)\)\=:\(E967\|E964\)/ # ignore 967 endtry endfor enddef export def Update_highlights(id: number, key: string, highlights: list, start: number = 0, end: number = -1, priority: any = null, changedtick: any = null): void const bufnr = id == 0 ? bufnr('%') : id if bufloaded(bufnr) const tick = getbufvar(bufnr, 'changedtick') if type(changedtick) == v:t_number && changedtick != tick return endif const ns = Create_namespace(key) coc#api#Buf_clear_namespace(bufnr, ns, start, end) Add_highlights_timer(bufnr, ns, highlights, priority, tick) endif enddef # key could be -1 or string or number export def Buffer_update(bufnr: number, key: any, highlights: list, priority: any = null, changedtick: any = null): void if bufloaded(bufnr) const ns = Create_namespace(key) coc#api#Buf_clear_namespace(bufnr, ns, 0, -1) if empty(highlights) return endif const tick = getbufvar(bufnr, 'changedtick', 0) if type(changedtick) == v:t_number && tick != changedtick return endif # highlight current region first const winid = bufwinid(bufnr) if winid == -1 Add_highlights_timer(bufnr, ns, highlights, priority, tick) else const info = getwininfo(winid)->get(0, {}) const topline = info.topline const botline = info.botline if topline <= 5 Add_highlights_timer(bufnr, ns, highlights, priority, tick) else final curr_hls = [] final other_hls = [] for hl in highlights const lnum = type(hl) == v:t_list ? hl[1] + 1 : hl.lnum + 1 if lnum >= topline && lnum <= botline add(curr_hls, hl) else add(other_hls, hl) endif endfor const hls = extend(curr_hls, other_hls) Add_highlights_timer(bufnr, ns, hls, priority, tick) endif endif endif enddef export def Highlight_ranges(id: number, key: any, hlGroup: string, ranges: list, opts: dict = {}): void const bufnr = id == 0 ? bufnr('%') : id if bufloaded(bufnr) final highlights: list = [] if get(opts, 'clear', false) == true const ns = Create_namespace(key) coc#api#Buf_clear_namespace(bufnr, ns, 0, -1) endif for range in ranges const start_pos = range.start const end_pos = range.end const lines = getbufline(bufnr, start_pos.line + 1, end_pos.line + 1) for index in range(start_pos.line, end_pos.line) const line = get(lines, index - start_pos.line, '') if len(line) == 0 continue endif const colStart = index == start_pos.line ? coc#text#Byte_index(line, start_pos.character) : 0 const colEnd = index == end_pos.line ? coc#text#Byte_index(line, end_pos.character) : strlen(line) if colStart >= colEnd continue endif const combine = get(opts, 'combine', false) ? 1 : 0 const start_incl = get(opts, 'start_incl', false) ? 1 : 0 const end_incl = get(opts, 'end_incl', false) ? 1 : 0 add(highlights, [hlGroup, index, colStart, colEnd, combine, start_incl, end_incl]) endfor endfor const priority = has_key(opts, 'priority') ? opts.priority : 4096 Set_highlights(bufnr, key, highlights, priority) endif enddef export def Match_ranges(id: number, buf: number, ranges: list, hlGroup: string, priority: any = 99): list const winid = id == 0 ? win_getid() : id const bufnr = buf == 0 ? winbufnr(winid) : buf if empty(getwininfo(winid)) || (buf != 0 && winbufnr(winid) != buf) return [] endif final ids = [] final pos = [] for range in ranges const start_pos = range.start const end_pos = range.end const lines = getbufline(bufnr, start_pos.line + 1, end_pos.line + 1) for index in range(start_pos.line, end_pos.line) const line = get(lines, index - start_pos.line, '') if len(line) == 0 continue endif const colStart = index == start_pos.line ? coc#text#Byte_index(line, start_pos.character) : 0 const colEnd = index == end_pos.line ? coc#text#Byte_index(line, end_pos.character) : strlen(line) if colStart >= colEnd continue endif add(pos, [index + 1, colStart + 1, colEnd - colStart]) endfor endfor const count = len(pos) if count > 0 const pr = type(priority) == v:t_number ? priority : 99 const opts = {'window': winid} if count < 9 || has('patch-9.0.0622') ids->add(matchaddpos(hlGroup, pos, pr, -1, opts)) else # limit to 8 each time for i in range(0, count - 1, 8) const end = min([i + 8, count]) - 1 ids->add(matchaddpos(hlGroup, pos[i : end], pr, -1, opts)) endfor endif endif return ids enddef # key could be string or number, use -1 for all highlights. export def Get_highlights(bufnr: number, key: any, start: number, end: number): list if !bufloaded(bufnr) return [] endif const ns = type(key) == v:t_number ? key : Create_namespace(key) const types: list = coc#api#GetNamespaceTypes(ns) if empty(types) return [] endif final res: list = [] const endLnum: number = end == -1 ? -1 : end + 1 for prop in prop_list(start + 1, {'bufnr': bufnr, 'types': types, 'end_lnum': endLnum}) if prop.start == 0 || prop.end == 0 # multi line textprop are not supported, simply ignore it continue endif const startCol: number = prop.col - 1 const endCol: number = startCol + prop.length const hl = prop_type_get(prop.type)->get('highlight', '') add(res, [ hl, prop.lnum - 1, startCol, endCol, prop.id ]) endfor return res enddef export def Del_markers(bufnr: number, key: any, ids: list): void if bufloaded(bufnr) for id in ids prop_remove({'bufnr': bufnr, 'id': id}) endfor endif enddef # Can't use strdisplaywidth as it doesn't support bufnr def Calc_padding_size(bufnr: number, indent: string): number const tabSize: number = getbufvar(bufnr, '&shiftwidth') ?? getbufvar(bufnr, '&tabstop', 8) var padding: number = 0 for character in indent if character == "\t" padding += tabSize - (padding % tabSize) else padding += 1 endif endfor return padding enddef def Add_vtext_item(bufnr: number, ns: number, opts: dict, pre: string, priority: number): void var propColumn: number = get(opts, 'col', 0) const align = get(opts, 'text_align', 'after') const line = opts.line const blocks = opts.blocks var blockList: list> = blocks const virt_lines = get(opts, 'virt_lines', []) var isAboveBelow = align ==# 'above' || align ==# 'below' if !empty(blocks) && isAboveBelow # only first highlight can be used const highlightGroup: string = blocks[0][1] const text: string = blocks->mapnew((_, block: list): string => block[0])->join('') blockList = [[text, highlightGroup]] propColumn = 0 endif var first: bool = true final base: dict = { 'priority': priority } if propColumn == 0 && align != 'overlay' base.text_align = align endif if has_key(opts, 'text_wrap') base.text_wrap = opts.text_wrap endif var before: string = '' for blockItem in blockList const text = empty(before) ? blockItem[0] : $'{before}{blockItem[0]}' const highlightGroup: string = get(blockItem, 1, '') if empty(highlightGroup) # should be spaces before = text continue endif before = '' const type: string = coc#api#CreateType(ns, highlightGroup, opts) final propOpts: dict = extend({ 'text': text, 'type': type, 'bufnr': bufnr }, base) if first && propColumn == 0 # add a whitespace, same as neovim. if align ==# 'after' propOpts.text_padding_left = 1 elseif !empty(pre) && isAboveBelow propOpts.text_padding_left = Calc_padding_size(bufnr, pre) endif endif prop_add(line + 1, propColumn, propOpts) first = false endfor for item_list in virt_lines for [text, highlightGroup] in item_list const type: string = coc#api#CreateType(ns, highlightGroup, opts) final propOpts: dict = { 'text': text, 'type': type, 'bufnr': bufnr, 'text_align': 'below'} prop_add(line + 1, 0, propOpts) endfor endfor enddef export def Add_vtext(bufnr: number, ns: number, line: number, blocks: list>, opts: dict): void var propIndent: string = '' if get(opts, 'indent', false) propIndent = matchstr(get(getbufline(bufnr, line + 1), 0, ''), '^\s\+') endif final conf = {'line': line, 'blocks': blocks} Add_vtext_item(bufnr, ns, extend(conf, opts), propIndent, get(opts, 'priority', 0)) enddef def Add_vtext_items(bufnr: number, ns: number, items: list, indent: bool, priority: number): void const length = len(items) if length > 0 var buflines: list = [] var start = 0 var propIndent: string = '' if indent start = items[0].line const endLine = items[length - 1].line buflines = getbufline(bufnr, start + 1, endLine + 1) endif for item in items if indent propIndent = matchstr(buflines[item.line - start], '^\s\+') endif Add_vtext_item(bufnr, ns, item, propIndent, priority) endfor endif enddef def Add_vtexts_timer(bufnr: number, ns: number, items: list, indent: bool, priority: number, changedtick: number): void if !bufloaded(bufnr) || getbufvar(bufnr, 'changedtick', 0) != changedtick return endif if len(items) > maxCount const hls = items[ : maxCount - 1] const next = items[maxCount : ] Add_vtext_items(bufnr, ns, hls, indent, priority) timer_start(10, (_) => Add_vtexts_timer(bufnr, ns, next, indent, priority, changedtick)) else Add_vtext_items(bufnr, ns, items, indent, priority) endif enddef export def Set_virtual_texts(bufnr: number, ns: number, items: list, indent: bool, priority: number): void if bufloaded(bufnr) const changedtick = getbufvar(bufnr, 'changedtick', 0) Add_vtexts_timer(bufnr, ns, items, indent, priority, changedtick) endif enddef # Apply many text changes while preserve text props can be slow, def Apply_changes(bufnr: number, changes: list): void const start_time = reltime() const total = len(changes) var timeout: bool = false var i = total - 1 while i >= 0 const item = changes[i] # item is null for some unknown reason if !empty(item) coc#api#SetBufferText(bufnr, item[1], item[2], item[3], item[4], item[0]) endif i -= 1 endwhile const duration = (start_time->reltime()->reltimefloat()) * 1000 if duration > 200 maxEditCount = maxEditCount / 2 coc#api#EchoHl($'Text edits cost {float2nr(duration)}ms, consider configure g:coc_edits_maximum_count < {total}', 'WarningMsg') endif enddef # Replace text before cursor at current line, insert should not includes line break. # 0 based start col export def Set_lines(bufnr: number, changedtick: number, original: list, replacement: list, start: number, end: number, changes: any, cursor: any, col: any, linecount: number): void if bufloaded(bufnr) const current = bufnr == bufnr('%') const view = current ? winsaveview() : null var start_row: number = start var end_row: number = end var replace = copy(replacement) var finished: bool = false var change_list = copy(changes) var delta: number = 0 if current && type(col) == v:t_number delta = col('.') - col endif if changedtick != getbufvar(bufnr, 'changedtick') && end_row > start_row const line_delta = bufnr->getbufinfo()->get(0).linecount - linecount if line_delta == 0 # Check current line change first const curr_lines = getbufline(bufnr, start_row + 1, end_row) const pos = getpos('.') const row = current ? pos[1] - start_row - 1 : -1 for idx in range(0, len(curr_lines) - 1) var oldStr = get(original, idx, '') var newStr = get(curr_lines, idx, '') var replaceStr = get(replace, idx, null) var colIdx = idx == row ? pos[2] - 1 : -1 if oldStr !=# newStr && replaceStr != null if replaceStr ==# oldStr replaceStr = newStr else replaceStr = coc#text#DiffApply(oldStr, newStr, replaceStr, colIdx) endif if replaceStr != null replace[idx] = replaceStr endif change_list = [] endif endfor else # Check if change lines before or after # Consider changed before first if coc#text#LinesEqual(replace, getbufline(bufnr, start_row + 1 + line_delta, end_row + line_delta)) start_row += line_delta end_row += line_delta change_list = [] elseif !coc#text#LinesEqual(replace, getbufline(bufnr, start_row + 1, end_row)) return endif endif endif if !empty(change_list) && len(change_list) <= maxEditCount Apply_changes(bufnr, change_list) else coc#api#SetBufferLines(bufnr, start_row + 1, end_row, replace) endif if current winrestview(view) endif coc#api#OnTextChange(bufnr) if !empty(cursor) && current cursor(cursor[0], cursor[1] + delta) endif endif enddef defcompile ================================================ FILE: autoload/coc/vtext.vim ================================================ let s:is_vim = !has('nvim') " Add multiple virtual texts, use timer when needed. " bufnr - The buffer number " ns - Id created by Nvim_create_namespace() " items - list of item: " item.line - Zero based line number " item.blocks - List with [text, hl_group] " item.hl_mode - Default to 'combine'. " item.col - vim & nvim >= 0.10.0, default to 0. " item.virt_text_win_col - neovim only. " item.text_align - Could be 'after' 'right' 'below' 'above'. " item.text_wrap - Could be 'wrap' and 'truncate', vim9 only. " indent - Prepend indent of current line when true " priority - Highlight priority function! coc#vtext#set(bufnr, ns, items, indent, priority) abort try if s:is_vim call coc#vim9#Set_virtual_texts(a:bufnr, a:ns, a:items, a:indent, a:priority) else call v:lua.require('coc.vtext').set(a:bufnr, a:ns, a:items, a:indent, a:priority) endif catch /.*/ call coc#compat#send_error('coc#vtext#set', s:is_vim) endtry endfunction " Check virtual text of namespace exists function! coc#vtext#exists(bufnr, ns) abort if s:is_vim let types = coc#api#GetNamespaceTypes(a:ns) if empty(types) return 0 endif return !empty(prop_list(1, {'bufnr': a:bufnr, 'types': types, 'end_lnum': -1})) endif return !empty(nvim_buf_get_extmarks(a:bufnr, a:ns, [0, 0], [-1, -1], {})) endfunction " This function is called by buffer.setVirtualText " ns - Id created by coc#highlight#create_namespace() " line - Zero based line number " blocks - List with [text, hl_group] " opts.hl_mode - Default to 'combine'. " opts.col - vim & nvim >= 0.10.0, default to 0. " opts.virt_text_win_col - neovim only. " opts.text_align - Could be 'after' 'right' 'below' 'above', converted on neovim. " opts.text_wrap - Could be 'wrap' and 'truncate', vim9 only. " opts.indent - add indent when using 'above' and 'below' as text_align function! coc#vtext#add(bufnr, ns, line, blocks, opts) abort try if s:is_vim call coc#vim9#Add_vtext(a:bufnr, a:ns, a:line, a:blocks, a:opts) else call v:lua.require('coc.vtext').add(a:bufnr, a:ns, a:line, a:blocks, a:opts) endif catch /.*/ call coc#compat#send_error('coc#vtext#add', s:is_vim) endtry endfunction ================================================ FILE: autoload/coc/window.vim ================================================ let g:coc_max_treeview_width = get(g:, 'coc_max_treeview_width', 40) let s:is_vim = !has('nvim') " Get tabpagenr of winid, return -1 if window doesn't exist function! coc#window#tabnr(winid) abort " getwininfo not work with popup on vim if s:is_vim && index(popup_list(), a:winid) != -1 call win_execute(a:winid, 'let g:__coc_tabnr = tabpagenr()') let nr = g:__coc_tabnr unlet g:__coc_tabnr return nr endif let info = getwininfo(a:winid) return empty(info) ? -1 : info[0]['tabnr'] endfunction " (1, 0) based line, column function! coc#window#get_cursor(winid) abort if exists('*nvim_win_get_cursor') return nvim_win_get_cursor(a:winid) endif let pos = getcurpos(a:winid) return [pos[1], pos[2] - 1] endfunction " Check if winid visible on current tabpage function! coc#window#visible(winid) abort if s:is_vim if coc#window#tabnr(a:winid) != tabpagenr() return 0 endif " Check possible hidden popup try return get(popup_getpos(a:winid), 'visible', 0) == 1 catch /^Vim\%((\a\+)\)\=:E993/ return 1 endtry else if !nvim_win_is_valid(a:winid) return 0 endif return coc#window#tabnr(a:winid) == tabpagenr() endif endfunction " winid is popup and shown function! s:visible_popup(winid) abort if index(popup_list(), a:winid) != -1 return get(popup_getpos(a:winid), 'visible', 0) == 1 endif return 0 endfunction " Return default or v:null when name or window doesn't exist, " 'getwinvar' only works on window of current tab function! coc#window#get_var(winid, name, ...) abort let tabnr = coc#window#tabnr(a:winid) if tabnr == -1 return get(a:, 1, v:null) endif return gettabwinvar(tabnr, a:winid, a:name, get(a:, 1, v:null)) endfunction " Not throw like setwinvar function! coc#window#set_var(winid, name, value) abort let tabnr = coc#window#tabnr(a:winid) if tabnr == -1 return endif call settabwinvar(tabnr, a:winid, a:name, a:value) endfunction function! coc#window#is_float(winid) abort if s:is_vim return index(popup_list(), a:winid) != -1 else if nvim_win_is_valid(a:winid) let config = nvim_win_get_config(a:winid) return !empty(get(config, 'relative', '')) endif endif return 0 endfunction " Reset current lnum & topline of window function! coc#window#restview(winid, lnum, topline) abort if empty(getwininfo(a:winid)) return endif if s:is_vim && s:visible_popup(a:winid) call popup_setoptions(a:winid, {'firstline': a:topline}) return endif call win_execute(a:winid, ['noa call winrestview({"lnum":'.a:lnum.',"topline":'.a:topline.'})']) endfunction function! coc#window#set_height(winid, height) abort if empty(getwininfo(a:winid)) return endif if !s:is_vim call nvim_win_set_height(a:winid, a:height) else if coc#window#is_float(a:winid) call popup_move(a:winid, {'minheight': a:height, 'maxheight': a:height}) else call win_execute(a:winid, 'noa resize '.a:height) endif endif endfunction function! coc#window#adjust_width(winid) abort let bufnr = winbufnr(a:winid) if bufloaded(bufnr) let maxwidth = 0 let lines = getbufline(bufnr, 1, '$') if len(lines) > 2 call win_execute(a:winid, 'setl nowrap') for line in lines let w = strwidth(line) if w > maxwidth let maxwidth = w endif endfor endif if maxwidth > winwidth(a:winid) call win_execute(a:winid, 'vertical resize '.min([maxwidth, g:coc_max_treeview_width])) endif endif endfunction " Get single window by window variable, current tab only function! coc#window#find(key, val) abort for i in range(1, winnr('$')) let res = getwinvar(i, a:key) if res == a:val return win_getid(i) endif endfor return -1 endfunction " Visible buffer numbers function! coc#window#bufnrs() abort let winids = map(getwininfo(), 'v:val["winid"]') return uniq(map(winids, 'winbufnr(v:val)')) endfunction function! coc#window#buf_winid(bufnr) abort let winids = map(getwininfo(), 'v:val["winid"]') for winid in winids if winbufnr(winid) == a:bufnr return winid endif endfor return -1 endfunction " Avoid errors function! coc#window#close(winid) abort if empty(a:winid) || a:winid == -1 return endif if coc#window#is_float(a:winid) call coc#float#close(a:winid) return endif call win_execute(a:winid, 'noa close!', 'silent!') endfunction function! coc#window#visible_range(winid) abort let winid = a:winid == 0 ? win_getid() : a:winid let info = get(getwininfo(winid), 0, v:null) if empty(info) return v:null endif return [info['topline'], info['botline']] endfunction function! coc#window#visible_ranges(bufnr) abort let wins = gettabinfo(tabpagenr())[0]['windows'] let res = [] for id in wins let info = getwininfo(id)[0] if info['bufnr'] == a:bufnr call add(res, [info['topline'], info['botline']]) endif endfor return res endfunction " Clear matches by hlGroup regexp. function! coc#window#clear_match_group(winid, match) abort let winid = a:winid == 0 ? win_getid() : a:winid if !empty(getwininfo(winid)) let arr = filter(getmatches(winid), 'v:val["group"] =~# "'.a:match.'"') for item in arr call matchdelete(item['id'], winid) endfor endif endfunction " Clear matches by match ids, use 0 for current win. function! coc#window#clear_matches(winid, ids) abort let winid = a:winid == 0 ? win_getid() : a:winid if !empty(getwininfo(winid)) for id in a:ids try call matchdelete(id, winid) catch /^Vim\%((\a\+)\)\=:E803/ " ignore endtry endfor endif endfunction ================================================ FILE: autoload/coc.vim ================================================ scriptencoding utf-8 let g:coc_user_config = get(g:, 'coc_user_config', {}) let g:coc_global_extensions = get(g:, 'coc_global_extensions', []) let g:coc_selected_text = '' let g:coc_vim_commands = [] let s:watched_keys = [] let s:is_vim = !has('nvim') let s:utf = has('nvim') || &encoding =~# '^utf' let s:error_sign = get(g:, 'coc_status_error_sign', has('mac') && s:utf ? "\u274c " : 'E ') let s:warning_sign = get(g:, 'coc_status_warning_sign', has('mac') && s:utf ? "\u26a0\ufe0f " : 'W ') let s:select_api = exists('*nvim_select_popupmenu_item') let s:callbacks = {} let s:fns = ['init', 'complete', 'should_complete', 'refresh', 'get_startcol', 'on_complete', 'on_enter'] let s:all_fns = s:fns + map(copy(s:fns), 'toupper(strpart(v:val, 0, 1)) . strpart(v:val, 1)') function! coc#expandable() abort return coc#rpc#request('snippetCheck', [1, 0]) endfunction function! coc#jumpable() abort return coc#rpc#request('snippetCheck', [0, 1]) endfunction function! coc#expandableOrJumpable() abort return coc#rpc#request('snippetCheck', [1, 1]) endfunction " Only clear augroup starts with coc function! coc#clearGroups(prefix) abort for group in getcompletion('coc', 'augroup') if group =~# '^' . a:prefix execute 'autocmd! ' . group endif endfor endfunction " add vim command to CocCommand list function! coc#add_command(id, cmd, ...) let config = {'id':a:id, 'cmd':a:cmd, 'title': get(a:,1,'')} call add(g:coc_vim_commands, config) if !coc#rpc#ready() | return | endif call coc#rpc#notify('addCommand', [config]) endfunction function! coc#on_enter() call coc#rpc#notify('CocAutocmd', ['Enter', bufnr('%')]) return '' endfunction function! coc#_insert_key(method, key, ...) abort let prefix = '' if get(a:, 1, 1) if coc#pum#visible() let prefix = "\=coc#pum#close()\" elseif pumvisible() let prefix = "\\" endif endif return prefix."\=coc#rpc#".a:method."('doKeymap', ['".a:key."'])\" endfunction " used for statusline function! coc#status(...) let info = get(b:, 'coc_diagnostic_info', {}) let msgs = [] if !empty(info) && get(info, 'error', 0) call add(msgs, s:error_sign . info['error']) endif if !empty(info) && get(info, 'warning', 0) call add(msgs, s:warning_sign . info['warning']) endif let status = get(g:, 'coc_status', '') if get(a:, 1, 0) let status = substitute(status, '%', '%%', 'g') endif return trim(join(msgs, ' ') . ' ' . status) endfunction function! coc#config(section, value) let g:coc_user_config[a:section] = a:value call coc#rpc#notify('updateConfig', [a:section, a:value]) endfunction " Deprecated, use variable instead. function! coc#add_extension(...) if a:0 == 0 | return | endif call extend(g:coc_global_extensions, a:000) endfunction function! coc#_watch(key) if s:is_vim | return | endif if index(s:watched_keys, a:key) == -1 call add(s:watched_keys, a:key) call dictwatcheradd(g:, a:key, function('s:GlobalChange')) endif endfunction function! coc#_unwatch(key) if s:is_vim | return | endif let idx = index(s:watched_keys, a:key) if idx != -1 call remove(s:watched_keys, idx) call dictwatcherdel(g:, a:key, function('s:GlobalChange')) endif endfunction function! s:GlobalChange(dict, key, val) call coc#rpc#notify('GlobalChange', [a:key, get(a:val, 'old', v:null), get(a:val, 'new', v:null)]) endfunction function! coc#on_notify(id, method, Cb) let key = a:id. '-'.a:method let s:callbacks[key] = a:Cb call coc#rpc#notify('registerNotification', [a:id, a:method]) endfunction function! coc#do_notify(id, method, result) let key = a:id. '-'.a:method let Fn = s:callbacks[key] if !empty(Fn) call Fn(a:result) endif endfunction function! coc#start(...) call CocActionAsync('startCompletion', get(a:, 1, {})) return '' endfunction " Could be used by coc extensions function! coc#_cancel(...) call coc#pum#close() endfunction function! coc#refresh() abort return "\=coc#start()\" endfunction function! coc#_select_confirm() abort return "\=coc#pum#select_confirm()\" endfunction function! coc#_suggest_variables() abort return { \ 'disable': get(b:, 'coc_suggest_disable', 0), \ 'disabled_sources': get(b:, 'coc_disabled_sources', []), \ 'blacklist': get(b:, 'coc_suggest_blacklist', []), \ } endfunction function! coc#_remote_fns(name) let res = [] for fn in s:all_fns if exists('*coc#source#'.a:name.'#'.fn) call add(res, fn) endif endfor return res endfunction function! coc#_do_complete(name, opt, cb) abort let method = get(a:opt, 'vim9', v:false) ? 'Complete' : 'complete' let handler = 'coc#source#'.a:name.'#'.method let l:Cb = {res -> a:cb(v:null, res)} let args = [a:opt, l:Cb] call call(handler, args) endfunction ================================================ FILE: autoload/health/coc.vim ================================================ scriptencoding utf-8 let s:root = expand(':h:h:h') function! s:report_ok(report) abort if has('nvim-0.10') call v:lua.vim.health.ok(a:report) else call health#report_ok(a:report) endif endfunction function! s:report_error(report, advises) abort if has('nvim-0.10') call v:lua.vim.health.error(a:report, a:advises) else call health#report_error(a:report, a:advises) endif endfunction function! s:report_warn(report) abort if has('nvim-0.10') call v:lua.vim.health.warn(a:report) else call health#report_warn(a:report) endif endfunction function! s:checkVim(test, name, patchlevel) abort if a:test if !has(a:patchlevel) call s:report_error(a:name . ' version not satisfied, ' . a:patchlevel . ' and above required') return 0 else call s:report_ok(a:name . ' version satisfied') return 1 endif endif return 0 endfunction function! s:checkEnvironment() abort let valid \ = s:checkVim(has('nvim'), 'nvim', 'nvim-0.8.0') \ + s:checkVim(!has('nvim'), 'vim', 'patch-9.0.0438') let node = get(g:, 'coc_node_path', $COC_NODE_PATH == '' ? 'node' : $COC_NODE_PATH) if !executable(node) let valid = 0 call s:report_error('Executable node.js not found, install node.js from http://nodejs.org/') endif let output = system(node . ' --version') if v:shell_error && output !=# "" let valid = 0 call s:report_error(output) endif let ms = matchlist(output, 'v\(\d\+\).\(\d\+\).\(\d\+\)') if empty(ms) let valid = 0 call s:report_error('Unable to detect version of node, make sure your node executable is http://nodejs.org/') elseif str2nr(ms[1]) < 16 || (str2nr(ms[1]) == 16 && str2nr(ms[2]) < 18) let valid = 0 call s:report_warn('Node.js version '.trim(output).' < 16.18.0, please upgrade node.js') endif if valid call s:report_ok('Environment check passed') endif if has('pythonx') try silent pyx print("") catch /.*/ call s:report_warn('pyx command not work, some extensions may fail to work, checkout ":h pythonx"') if has('nvim') call s:report_warn('Install pynvim by command: `pip install pynvim --upgrade`') endif endtry endif return valid endfunction function! s:checkCommand() let file = s:root.'/build/index.js' if filereadable(file) call s:report_ok('Javascript bundle build/index.js found') else call s:report_error('Javascript entry not found, please compile coc.nvim by esbuild.') endif endfunction function! s:checkAutocmd() let cmds = ['CursorHold', 'CursorHoldI', 'CursorMovedI', 'InsertCharPre', 'TextChangedI'] for cmd in cmds let lines = split(execute('verbose autocmd '.cmd), '\n') let n = 0 for line in lines if line =~# 'CocAction(' && n < len(lines) - 1 let next = lines[n + 1] let ms = matchlist(next, 'Last set from \(.*\)') if !empty(ms) call s:report_warn('Use CocActionAsync to replace CocAction for better performance on '.cmd) call s:report_warn('Checkout the file '.ms[1]) endif endif let n = n + 1 endfor endfor endfunction function! s:checkInitialize() abort if get(g:, 'coc_start_at_startup', 1) == 0 call s:report_warn('coc.nvim was disabled on startup, run :CocStart to start manually') return 1 endif if coc#client#is_running('coc') call s:report_ok('Service started') return 1 endif call s:report_error('service could not be initialized', [ \ 'Use command ":messages" to get error messages.', \ 'Open an issue at https://github.com/neoclide/coc.nvim/issues for feedback.' \]) return 0 endfunction function! health#coc#check() abort call s:checkEnvironment() call s:checkCommand() call s:checkInitialize() call s:checkAutocmd() endfunction ================================================ FILE: bin/prompt.js ================================================ /* * Used for prompt popup on vim */ const readline = require("readline") const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, escapeCodeTimeout: 0, prompt: '' }) let value = process.argv[2] let placeholder = process.argv[3] let clear = false if (value) { rl.write(value) } else if (placeholder) { clear = true rl.write('\x1B[90m' + placeholder + '\x1B[39m') rl.write('', {ctrl: true, name: 'a'}) } rl.on('line', input => { send(['confirm', clear ? '' : input]) process.exit() }) let original_ttyWrite = rl._ttyWrite rl._ttyWrite = function (code, key) { if (key.name === 'enter') { send(['send', '']) return '' } original_ttyWrite.apply(rl, arguments) if (clear && rl.line.includes('\x1B')) { clear = false rl.write('', {ctrl: true, name: 'k'}) return } send(['change', rl.line]) } function createSequences(str) { return '\x1b]51;' + str + '\x07' } function send(args) { process.stdout.write(createSequences(JSON.stringify(['call', 'CocPopupCallback', args]))) } process.stdin.on('keypress', (e, key) => { if (key) { let k = getKey(key) if (k == '') { return } if (k == '') { send(['exit', '']) process.exit() } if (k) { send(['send', k]) return } } }) function getKey(key) { if (key.ctrl === true) { if (key.name == 'n') { return '' } if (key.name == 'p') { return '' } if (key.name == 'j') { return '' } if (key.name == 'k') { return '' } if (key.name == 'f') { return '' } if (key.name == 't') { return '' } if (key.name == 'b') { return '' } if (key.sequence == '\x00') { return '' } } if (key.sequence == '\u001b') { return '' } if (key.sequence == '\r') { return '' } if (key.sequence == '\t') { return key.shift ? '' : '' } if (key.name == 'up') { return '' } if (key.name == 'down') { return '' } return '' } ================================================ FILE: bin/terminateProcess.sh ================================================ #!/bin/bash terminateTree() { for cpid in $(pgrep -P $1); do terminateTree $cpid done kill -9 $1 > /dev/null 2>&1 } for pid in $*; do terminateTree $pid done ================================================ FILE: data/schema.json ================================================ { "description": "Configuration file for coc.nvim", "additionalProperties": false, "definitions": { "floatConfig": { "type": "object", "properties": { "border": { "type": "boolean", "default": false, "description": "Set to true to use borders." }, "rounded": { "type": "boolean", "default": false, "description": "Use rounded borders when border is true." }, "highlight": { "type": "string", "default": "CocFloating", "description": "Background highlight group of float window." }, "title": { "type": "string", "default": "", "description": "Title used by float window." }, "borderhighlight": { "type": "string", "default": "CocFloatBorder", "description": "Border highlight group of float window." }, "close": { "type": "boolean", "default": false, "description": "Set to true to draw close icon" }, "maxWidth": { "type": "integer", "description": "Maximum width of float window, include border." }, "maxHeight": { "type": "integer", "minimum": 2, "description": "Maximum height of float window, include border." }, "focusable": { "type": "boolean", "default": true, "description": "Enable focus by user actions (wincmds, mouse events), neovim only." }, "shadow": { "type": "boolean", "default": false, "description": "Drop shadow effect by blending with the background, neovim only." }, "winblend": { "type": "integer", "default": 0, "minimum": 0, "maximum": 100, "description": "Enables pseudo-transparency by set 'winblend' option of window, neovim only." }, "position": { "type": "string", "default": "auto", "description": "Controls how floating windows are positioned. When set to `'fixed'`, the window will be positioned according to the `top`, `bottom`, `left`, and `right` settings. When set to `'auto'`, the window follows the default position.", "enum": ["fixed", "auto"] }, "top": { "type": "number", "description": "Distance from the top of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Will be ignored if `bottom` is set." }, "bottom": { "type": "number", "description": "Distance from the bottom of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Takes precedence over `top` if both are set." }, "left": { "type": "number", "description": "Distance from the left edge of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Will be ignored if `right` is set." }, "right": { "type": "number", "description": "Distance from the right edge of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Takes precedence over `left` if both are set." } } }, "languageserver.enable": { "type": "boolean", "description": "Enable the languageserver, restart coc.nvim required after change.", "default": true }, "languageserver.filetypes": { "type": "array", "default": [], "description": "Supported filetypes, add * in array for all filetypes.", "items": { "type": "string" } }, "languageserver.maxRestartCount": { "type": "integer", "default": 4, "minimum": 1, "description": "Maximum restart count when server closed in the last 3 minutes." }, "languageserver.cwd": { "type": "string", "default": "", "description": "Working directory of languageserver, absolute path or relative to workspace folder, use workspace root by default" }, "languageserver.settings": { "type": "object", "default": {}, "description": "Settings of languageserver" }, "languageserver.initializationOptions": { "type": "object", "default": {}, "description": "initializationOptions passed to languageserver" }, "languageserver.env": { "type": "object", "default": null, "description": "Environment variables for child process." }, "languageserver.stdioEncoding": { "type": "string", "default": "utf8", "description": "Encoding used for stdio of child process." }, "languageserver.rootPatterns": { "type": "array", "default": [], "description": "Root patterns used to resolve rootPath from current file, default to workspace root", "items": { "type": "string" } }, "languageserver.requireRootPattern": { "type": "boolean", "default": false, "description": "If true, doesn't start server when root pattern not found." }, "languageserver.ignoredRootPaths": { "type": "array", "default": [], "description": "Absolute root paths that language server should not use as rootPath, higher priority than rootPatterns.", "items": { "type": "string" } }, "languageserver.additionalSchemes": { "type": "array", "default": [], "description": "Additional URI schemes, default schemes including file & untitled.", "items": { "type": "string" } }, "languageserver.revealOutputChannelOn": { "type": "string", "default": "never", "description": "Configure message level to show the output channel buffer", "enum": ["info", "warn", "error", "never"] }, "languageserver.progressOnInitialization": { "type": "boolean", "default": false, "description": "Enable progress report on languageserver initialize." }, "languageserver.trace.server": { "type": "string", "default": "off", "enum": ["off", "messages", "verbose"], "description": "Trace level of communication between server and client" }, "languageserver.trace.server.verbosity": { "type": "string", "default": "off", "enum": ["off", "messages", "compact", "verbose"], "description": "Trace level of communication between server and client" }, "languageserver.trace.server.format": { "type": "string", "default": "text", "enum": ["text", "json"], "description": "Text format of trace messages." }, "languageserver.disableDynamicRegister": { "type": "boolean", "default": false, "description": "Disable dynamic registerCapability feature for this languageserver to avoid duplicate feature registration." }, "languageserver.disableSnippetCompletion": { "type": "boolean", "default": false, "description": "Disable completion snippet feature for this languageserver, the languageserver may not respect it." }, "languageserver.disabledFeatures": { "type": "array", "default": [], "description": "Disabled features for this languageserver.", "items": { "type": "string", "enum": [ "completion", "configuration", "workspaceFolders", "diagnostics", "willSave", "willSaveUntil", "didSaveTextDocument", "fileSystemWatcher", "hover", "signatureHelp", "definition", "references", "documentHighlight", "documentSymbol", "workspaceSymbol", "codeAction", "codeLens", "formatting", "documentFormatting", "documentRangeFormatting", "documentOnTypeFormatting", "rename", "documentLink", "executeCommand", "pullConfiguration", "typeDefinition", "implementation", "declaration", "color", "foldingRange", "selectionRange", "progress", "callHierarchy", "linkedEditing", "inlayHint", "inlineValue", "typeHierarchy", "pullDiagnostic", "fileEvents", "semanticTokens" ] } }, "languageserver.formatterPriority": { "type": "integer", "default": 0, "description": "Priority of this languageserver's formatter." }, "languageServerSocket": { "type": "object", "required": ["port", "filetypes"], "additionalProperties": false, "properties": { "port": { "type": "integer", "description": "Port number of socket server" }, "host": { "type": "string", "default": "127.0.0.1", "description": "Host of server" }, "enable": { "$ref": "#/definitions/languageserver.enable" }, "disableSnippetCompletion": { "$ref": "#/definitions/languageserver.disableSnippetCompletion" }, "disableDynamicRegister": { "$ref": "#/definitions/languageserver.disableDynamicRegister" }, "disabledFeatures": { "$ref": "#/definitions/languageserver.disabledFeatures" }, "formatterPriority": { "$ref": "#/definitions/languageserver.formatterPriority" }, "rootPatterns": { "$ref": "#/definitions/languageserver.rootPatterns" }, "requireRootPattern": { "$ref": "#/definitions/languageserver.requireRootPattern" }, "ignoredRootPaths": { "$ref": "#/definitions/languageserver.ignoredRootPaths" }, "maxRestartCount": { "$ref": "#/definitions/languageserver.maxRestartCount" }, "filetypes": { "$ref": "#/definitions/languageserver.filetypes" }, "additionalSchemes": { "$ref": "#/definitions/languageserver.additionalSchemes" }, "revealOutputChannelOn": { "$ref": "#/definitions/languageserver.revealOutputChannelOn" }, "progressOnInitialization": { "$ref": "#/definitions/languageserver.progressOnInitialization" }, "initializationOptions": { "$ref": "#/definitions/languageserver.initializationOptions" }, "settings": { "$ref": "#/definitions/languageserver.settings" }, "stdioEncoding": { "$ref": "#/definitions/languageserver.stdioEncoding" }, "trace.server": { "$ref": "#/definitions/languageserver.trace.server" }, "trace.server.verbosity": { "$ref": "#/definitions/languageserver.trace.server.verbosity" }, "trace.server.format": { "$ref": "#/definitions/languageserver.trace.server.format" } } }, "languageServerModule": { "type": "object", "required": ["module", "filetypes"], "additionalProperties": false, "properties": { "module": { "type": "string", "default": "", "description": "Absolute path of Javascript file, should works in IPC mode" }, "args": { "type": "array", "default": [], "description": "Extra arguments of module", "items": { "type": "string" } }, "runtime": { "type": "string", "default": "", "description": "Absolute path of node runtime." }, "execArgv": { "type": "array", "default": [], "description": "ARGV passed to node when using module, normally used for debugging, ex: [\"--nolazy\", \"--inspect-brk=6045\"]", "items": { "type": "string" } }, "transport": { "type": "string", "default": "ipc", "description": "Transport kind used by server, could be 'ipc', 'stdio', 'socket' and 'pipe'", "enum": ["ipc", "stdio", "socket", "pipe"] }, "transportPort": { "type": "integer", "description": "Port number used when transport is 'socket'" }, "enable": { "$ref": "#/definitions/languageserver.enable" }, "disableSnippetCompletion": { "$ref": "#/definitions/languageserver.disableSnippetCompletion" }, "disableDynamicRegister": { "$ref": "#/definitions/languageserver.disableDynamicRegister" }, "disabledFeatures": { "$ref": "#/definitions/languageserver.disabledFeatures" }, "formatterPriority": { "$ref": "#/definitions/languageserver.formatterPriority" }, "rootPatterns": { "$ref": "#/definitions/languageserver.rootPatterns" }, "requireRootPattern": { "$ref": "#/definitions/languageserver.requireRootPattern" }, "ignoredRootPaths": { "$ref": "#/definitions/languageserver.ignoredRootPaths" }, "maxRestartCount": { "$ref": "#/definitions/languageserver.maxRestartCount" }, "filetypes": { "$ref": "#/definitions/languageserver.filetypes" }, "additionalSchemes": { "$ref": "#/definitions/languageserver.additionalSchemes" }, "revealOutputChannelOn": { "$ref": "#/definitions/languageserver.revealOutputChannelOn" }, "progressOnInitialization": { "$ref": "#/definitions/languageserver.progressOnInitialization" }, "initializationOptions": { "$ref": "#/definitions/languageserver.initializationOptions" }, "settings": { "$ref": "#/definitions/languageserver.settings" }, "stdioEncoding": { "$ref": "#/definitions/languageserver.stdioEncoding" }, "trace.server": { "$ref": "#/definitions/languageserver.trace.server" }, "trace.server.verbosity": { "$ref": "#/definitions/languageserver.trace.server.verbosity" }, "trace.server.format": { "$ref": "#/definitions/languageserver.trace.server.format" } } }, "languageServerCommand": { "type": "object", "required": ["command", "filetypes"], "additionalProperties": false, "properties": { "command": { "type": "string", "default": "", "description": "Executable in $PATH to start languageserver, should not used with module" }, "args": { "type": "array", "default": [], "description": "Arguments of command", "items": { "type": "string" } }, "detached": { "type": "boolean", "default": false, "description": "Detach the languageserver process" }, "shell": { "type": ["boolean", "string"], "default": false, "description": "If true runs command inside of a shell, always true on windows." }, "enable": { "$ref": "#/definitions/languageserver.enable" }, "env": { "$ref": "#/definitions/languageserver.env" }, "disableSnippetCompletion": { "$ref": "#/definitions/languageserver.disableSnippetCompletion" }, "disableDynamicRegister": { "$ref": "#/definitions/languageserver.disableDynamicRegister" }, "disabledFeatures": { "$ref": "#/definitions/languageserver.disabledFeatures" }, "formatterPriority": { "$ref": "#/definitions/languageserver.formatterPriority" }, "rootPatterns": { "$ref": "#/definitions/languageserver.rootPatterns" }, "requireRootPattern": { "$ref": "#/definitions/languageserver.requireRootPattern" }, "ignoredRootPaths": { "$ref": "#/definitions/languageserver.ignoredRootPaths" }, "maxRestartCount": { "$ref": "#/definitions/languageserver.maxRestartCount" }, "filetypes": { "$ref": "#/definitions/languageserver.filetypes" }, "additionalSchemes": { "$ref": "#/definitions/languageserver.additionalSchemes" }, "revealOutputChannelOn": { "$ref": "#/definitions/languageserver.revealOutputChannelOn" }, "progressOnInitialization": { "$ref": "#/definitions/languageserver.progressOnInitialization" }, "initializationOptions": { "$ref": "#/definitions/languageserver.initializationOptions" }, "settings": { "$ref": "#/definitions/languageserver.settings" }, "stdioEncoding": { "$ref": "#/definitions/languageserver.stdioEncoding" }, "trace.server": { "$ref": "#/definitions/languageserver.trace.server" }, "trace.server.verbosity": { "$ref": "#/definitions/languageserver.trace.server.verbosity" }, "trace.server.format": { "$ref": "#/definitions/languageserver.trace.server.format" } } } }, "properties": { "callHierarchy.enableTooltip": { "type": "boolean", "scope": "application", "default": true, "description": "Enable tooltip to show relative filepath of call hierarchy." }, "callHierarchy.openCommand": { "type": "string", "scope": "application", "default": "edit", "description": "Open command for callHierarchy tree view." }, "callHierarchy.splitCommand": { "type": "string", "scope": "application", "default": "botright 30vs", "description": "Window split command used by callHierarchy tree view." }, "coc.preferences.rootPatterns": { "type": ["array", "null"], "default": null, "scope": "application", "description": "Root patterns to resolve workspaceFolder from parent folders of opened files, resolved from up to down.", "deprecationMessage": "Use 'workspace.rootPatterns' instead.", "items": { "type": "string" } }, "coc.preferences.bracketEnterImprove": { "type": "boolean", "scope": "language-overridable", "description": "Improve enter inside bracket `<> {} [] ()` by add new empty line below and place cursor to it. Works with `coc#on_enter()`", "default": true }, "coc.preferences.currentFunctionSymbolAutoUpdate": { "type": "boolean", "scope": "language-overridable", "description": "Automatically update the value of b:coc_current_function on CursorMove event", "default": false }, "coc.preferences.currentFunctionSymbolDebounceTime": { "type": "number", "scope": "application", "description": "Set debounce timer for the update of b:coc_current_function on CursorMove event", "default": 300 }, "coc.preferences.enableLinkedEditing": { "type": "boolean", "scope": "language-overridable", "default": false, "description": "Enable linked editing support." }, "coc.preferences.enableMarkdown": { "type": "boolean", "scope": "application", "description": "Tell the language server that markdown text format is supported, note that markdown text may not rendered as expected.", "default": true }, "coc.preferences.enableMessageDialog": { "type": "boolean", "scope": "application", "default": false, "deprecationMessage": "Use configuration 'coc.preferences.messageDialogKind' instead.", "description": "Enable interactive messages in notification dialog, or fallback to native vim confirm action." }, "coc.preferences.messageDialogKind": { "type": "string", "scope": "application", "default": "confirm", "description": "Method to use when showing interactive messages to the user", "enum": ["notification", "menu", "confirm"] }, "coc.preferences.messageReportKind": { "type": "string", "scope": "application", "default": "echo", "description": "Method to use when printing or showing plain user messages", "enum": ["notification", "echo"] }, "coc.preferences.excludeImageLinksInMarkdownDocument": { "type": "boolean", "description": "Exclude image links from markdown text in float window.", "scope": "application", "default": true }, "coc.preferences.enableGFMBreaksInMarkdownDocument": { "type": "boolean", "description": "Exclude GFM breaks in markdown document.", "scope": "application", "default": true }, "coc.preferences.extensionUpdateCheck": { "type": "string", "scope": "application", "default": "never", "description": "Interval for check extension update, could be daily, weekly, never", "deprecationMessage": "Use configuration 'extensions.updateCheck' instead.", "enum": ["daily", "weekly", "never"] }, "coc.preferences.extensionUpdateUIInTab": { "type": "boolean", "scope": "application", "default": false, "deprecationMessage": "Use configuration 'extensions.updateUIInTab' instead.", "description": "Display extension updating UI in new vim tab" }, "coc.preferences.silentAutoupdate": { "type": "boolean", "description": "Not open split window with update status when performing auto update.", "deprecationMessage": "Use configuration 'extensions.silentAutoupdate' instead.", "scope": "application", "default": true }, "coc.preferences.floatActions": { "type": "boolean", "scope": "application", "description": "Set to false to disable float/popup support for actions menu.", "default": true }, "coc.preferences.autoApplySingleQuickfix": { "type": "boolean", "scope": "application", "description": "Automatically apply the single quickfix action .", "default": true }, "coc.preferences.formatOnSave": { "type": "boolean", "description": "Set to true to enable formatting on save.", "scope": "language-overridable", "default": false }, "coc.preferences.formatOnSaveTimeout": { "type": "integer", "scope": "language-overridable", "description": "How long before the format command run on save times out.", "default": 500, "minimum": 200, "maximum": 5000 }, "coc.preferences.formatOnSaveFiletypes": { "type": ["null", "array"], "scope": "resource", "default": null, "description": "Filetypes that should run format on save.", "deprecationMessage": "Use 'coc.preferences.formatOnSave' as language override configuration instead, see :h coc-configuration-scope", "items": { "type": "string" } }, "coc.preferences.formatOnType": { "type": "boolean", "description": "Set to true to enable formatting on typing.", "scope": "language-overridable", "default": false }, "coc.preferences.formatOnTypeFiletypes": { "type": ["null", "array"], "default": null, "scope": "resource", "description": "Filetypes that should run format on typing, only works when `coc.preferences.formatOnType` is `true`", "deprecationMessage": "Use 'coc.preferences.formatOnType' as language override configuration instead, see :h coc-configuration-scope", "items": { "type": "string" } }, "coc.preferences.formatterExtension": { "type": ["null", "string"], "default": null, "scope": "language-overridable", "description": "Extension used for formatting documents. When set to null, the formatter with highest priority is used." }, "coc.preferences.jumpCommand": { "anyOf": [ { "type": "string", "enum": ["edit", "split", "vsplit", "tabe", "drop", "tab drop", "pedit"] }, {"type": "string", "minimum": 1} ], "scope": "application", "description": "Command used for location jump, like goto definition, goto references etc. Can be also a custom command that gives file as an argument.", "default": "edit" }, "coc.preferences.maxFileSize": { "type": "string", "scope": "application", "default": "10MB", "description": "Maximum file size in bytes that coc.nvim should handle, default '10MB'" }, "coc.preferences.messageLevel": { "type": "string", "scope": "application", "description": "Message level for filter echoed messages, could be 'more', 'warning' and 'error'", "default": "more", "enum": ["more", "warning", "error"] }, "coc.preferences.promptInput": { "type": "boolean", "description": "Use prompt buffer in float window for user input.", "scope": "application", "default": true }, "coc.preferences.renameFillCurrent": { "type": "boolean", "scope": "application", "default": true, "description": "Disable to stop Refactor-Rename float/popup window from populating with old name in the New Name field." }, "coc.preferences.useQuickfixForLocations": { "type": "boolean", "scope": "application", "description": "Use vim's quickfix list for jump locations,\n need restart on change.", "default": false }, "coc.preferences.watchmanPath": { "type": "string", "scope": "application", "deprecationMessage": "Use configuration \"fileSystemWatch.watchmanPath\" instead.", "description": "executable path for https://facebook.github.io/watchman/, detected from $PATH by default", "default": null }, "coc.preferences.willSaveHandlerTimeout": { "type": "integer", "scope": "application", "default": 500, "minimum": 200, "maximum": 5000, "description": "Will save handler timeout" }, "coc.preferences.tagDefinitionTimeout": { "type": "integer", "scope": "application", "default": 0, "description": "The timeout of CocTagFunc, default is infinity" }, "coc.source.around.disableSyntaxes": { "type": "array", "default": [], "scope": "application", "items": { "type": "string" } }, "coc.source.around.enable": { "type": "boolean", "scope": "application", "default": true }, "coc.source.around.priority": { "type": "integer", "scope": "application", "default": 1 }, "coc.source.around.shortcut": { "type": "string", "scope": "application", "default": "A" }, "coc.source.buffer.disableSyntaxes": { "type": "array", "default": [], "scope": "application", "items": { "type": "string" } }, "coc.source.buffer.enable": { "type": "boolean", "scope": "application", "default": true }, "coc.source.buffer.ignoreGitignore": { "type": "boolean", "default": true, "scope": "application", "description": "Ignore git ignored files for buffer words" }, "coc.source.buffer.priority": { "type": "integer", "scope": "application", "default": 1 }, "coc.source.buffer.shortcut": { "type": "string", "scope": "application", "default": "B" }, "coc.source.file.disableSyntaxes": { "type": "array", "default": [], "scope": "application", "items": { "type": "string" } }, "coc.source.file.enable": { "type": "boolean", "scope": "application", "default": true }, "coc.source.file.ignoreHidden": { "type": "boolean", "scope": "application", "default": true, "description": "Ignore completion for hidden files" }, "coc.source.file.ignorePatterns": { "type": "array", "scope": "application", "default": [], "description": "Ignore patterns of matcher", "items": { "type": "string" } }, "coc.source.file.priority": { "type": "integer", "scope": "application", "default": 10 }, "coc.source.file.shortcut": { "type": "string", "scope": "application", "default": "F" }, "coc.source.file.triggerCharacters": { "type": "array", "default": ["/", "\\"], "scope": "application", "items": { "type": "string" } }, "coc.source.file.trimSameExts": { "type": "array", "scope": "application", "default": [".ts", ".js"], "description": "Trim same extension on file completion", "items": { "type": "string" } }, "codeLens.enable": { "type": "boolean", "scope": "language-overridable", "description": "Enable codeLens feature, require neovim with set virtual text feature.", "default": false }, "codeLens.display": { "type": "boolean", "scope": "language-overridable", "default": true, "description": "Display codeLens." }, "codeLens.position": { "type": "string", "scope": "language-overridable", "enum": ["top", "eol", "right_align"], "description": "Display position of codeLens virtual text.", "default": "top" }, "codeLens.separator": { "type": "string", "scope": "language-overridable", "description": "Separator text for codeLens in virtual text", "default": "" }, "codeLens.subseparator": { "type": "string", "scope": "language-overridable", "description": "Subseparator between codeLenses in virtual text", "default": " | " }, "colors.enable": { "type": "boolean", "scope": "language-overridable", "description": "Enable colors highlight feature, for terminal vim, 'termguicolors' option should be enabled and the terminal support gui colors.", "default": false }, "colors.filetypes": { "type": ["array", "null"], "default": null, "scope": "resource", "deprecationMessage": "Use colors.enable as language override configuration instead, see :h coc-configuration-scope", "description": "Filetypes that should be enabled for colors highlight feature, use \"*\" for all filetypes.", "items": { "type": "string" } }, "colors.highlightPriority": { "type": "integer", "scope": "application", "description": "Priority for colors highlights, works on vim8 and neovim >= 0.6.0", "default": 1000, "maximum": 4096 }, "cursors.cancelKey": { "type": "string", "scope": "application", "default": "", "description": "Key used for cancel cursors session." }, "cursors.nextKey": { "type": "string", "scope": "application", "default": "", "description": "Key used for jump to next cursors position." }, "cursors.previousKey": { "type": "string", "scope": "application", "default": "", "description": "Key used for jump to previous cursors position." }, "cursors.wrapscan": { "type": "boolean", "scope": "application", "default": true, "description": "Searches wrap around the first or last cursors range." }, "diagnostic.autoRefresh": { "type": "boolean", "scope": "language-overridable", "description": "Enable automatically refresh diagnostics, use diagnosticRefresh action when it's disabled.", "default": true }, "diagnostic.checkCurrentLine": { "type": "boolean", "scope": "language-overridable", "description": "When enabled, show all diagnostics of current line if there are none at the current position.", "default": false }, "diagnostic.displayByAle": { "type": "boolean", "scope": "language-overridable", "description": "Use Ale, coc-diagnostics-shim.nvim, or other provider to display diagnostics in vim. This setting will disable diagnostic display using coc's handler. A restart required on change.", "default": false }, "diagnostic.displayByVimDiagnostic": { "type": "boolean", "scope": "language-overridable", "description": "Display diagnostics with nvim's `vim.diagnostic`. This setting will disable diagnostic display using coc's handler. A restart required on change. Neovim only.", "default": false }, "diagnostic.enable": { "type": "boolean", "scope": "language-overridable", "description": "Set to false to disable diagnostic display", "default": true }, "diagnostic.enableHighlightLineNumber": { "type": "boolean", "scope": "application", "default": true, "description": "Enable highlighting line numbers for diagnostics, only works with neovim." }, "diagnostic.enableMessage": { "type": "string", "scope": "application", "default": "always", "description": "When to enable show messages of diagnostics.", "enum": ["always", "jump", "never"] }, "diagnostic.enableSign": { "type": "boolean", "scope": "language-overridable", "default": true, "description": "Enable signs for diagnostics." }, "diagnostic.errorSign": { "type": "string", "scope": "application", "description": "Text of error sign", "default": ">>" }, "diagnostic.filetypeMap": { "type": "object", "scope": "application", "description": "A map between buffer filetype and the filetype assigned to diagnostics. To syntax highlight diagnostics with their parent buffer type use `\"default\": \"bufferType\"`", "default": {} }, "diagnostic.floatConfig": { "type": "object", "scope": "application", "description": "Configure float window style of diagnostic message.", "allOf": [{"$ref": "#/definitions/floatConfig"}], "additionalProperties": false, "properties": { "border": {}, "rounded": {}, "highlight": {}, "borderhighlight": {}, "title": {}, "close": {}, "maxHeight": {}, "maxWidth": {}, "winblend": {}, "focusable": {}, "shadow": {}, "position": {}, "top": {}, "bottom": {}, "left": {}, "right": {} } }, "diagnostic.format": { "type": "string", "scope": "language-overridable", "description": "Define the diagnostic format that shown in float window or echoed, available parts: source, code, severity, message", "default": "%message (%source%code)" }, "diagnostic.highlightLimit": { "type": "integer", "scope": "language-overridable", "description": "Limit count for highlighted diagnostics, too many diagnostic highlights could make vim stop responding", "default": 1000 }, "diagnostic.highlightPriority": { "type": "integer", "scope": "language-overridable", "description": "Priority for diagnostic highlights, works on vim8 and neovim >= 0.6.0", "default": 4096, "maximum": 4096, "minimum": 110 }, "diagnostic.hintSign": { "type": "string", "scope": "application", "description": "Text of hint sign", "default": ">>" }, "diagnostic.infoSign": { "type": "string", "scope": "application", "description": "Text of info sign", "default": ">>" }, "diagnostic.level": { "type": "string", "scope": "resource", "description": "Used for filter diagnostics by diagnostic severity.", "default": "hint", "enum": ["hint", "information", "warning", "error"] }, "diagnostic.locationlistLevel": { "type": ["string", "null"], "scope": "language-overridable", "description": "Filter diagnostics in locationlist.", "default": null, "enum": ["hint", "information", "warning", "error"] }, "diagnostic.locationlistUpdate": { "type": "boolean", "scope": "language-overridable", "description": "Update locationlist on diagnostics change, only works with locationlist opened by :CocDiagnostics command and first window of associated buffer.", "default": true }, "diagnostic.messageDelay": { "type": "integer", "scope": "application", "description": "How long to wait (in milliseconds) before displaying the diagnostic message with echo or float", "default": 200 }, "diagnostic.messageLevel": { "type": ["string", "null"], "scope": "language-overridable", "description": "Filter diagnostic message in float window/popup.", "default": null, "enum": ["hint", "information", "warning", "error"] }, "diagnostic.messageTarget": { "type": "string", "scope": "language-overridable", "description": "Diagnostic message target.", "default": "float", "enum": ["echo", "float"] }, "diagnostic.refreshOnInsertMode": { "type": "boolean", "scope": "language-overridable", "description": "Enable diagnostic refresh on insert mode, default false.", "default": false }, "diagnostic.showDeprecated": { "type": "boolean", "default": true, "scope": "language-overridable", "description": "Show diagnostics with deprecated tag." }, "diagnostic.showUnused": { "type": "boolean", "default": true, "scope": "language-overridable", "description": "Show diagnostics with unused tag, affects highlight, sign, virtual text, message" }, "diagnostic.signLevel": { "type": ["string", "null"], "scope": "language-overridable", "description": "Filter diagnostics displayed in signcolumn.", "default": null, "enum": ["hint", "information", "warning", "error"] }, "diagnostic.signPriority": { "type": "integer", "scope": "resource", "description": "Priority of diagnostic signs, default to 10", "default": 10 }, "diagnostic.virtualText": { "type": "boolean", "scope": "language-overridable", "description": "Use virtual text to display diagnostics.", "default": false }, "diagnostic.virtualTextAlign": { "type": "string", "scope": "language-overridable", "description": "Position of virtual text, default 'after'", "default": "after", "enum": ["after", "right", "below"] }, "diagnostic.virtualTextCurrentLineOnly": { "type": "boolean", "scope": "language-overridable", "description": "Only show virtualText diagnostic on current cursor line", "default": true }, "diagnostic.virtualTextFormat": { "type": "string", "scope": "language-overridable", "description": "Define the virtual text diagnostic format, available parts: source, code, severity, message", "default": "%message" }, "diagnostic.virtualTextLevel": { "type": ["string", "null"], "scope": "language-overridable", "description": "Filter diagnostic message in virtual text by level", "default": null, "enum": ["hint", "information", "warning", "error"] }, "diagnostic.virtualTextLimitInOneLine": { "type": "integer", "scope": "language-overridable", "minimum": 1, "description": "The maximum number of diagnostic messages to display in one line", "default": 999 }, "diagnostic.virtualTextLineSeparator": { "type": "string", "scope": "language-overridable", "description": "The text that will mark a line end from the diagnostic message", "default": " \\ " }, "diagnostic.virtualTextLines": { "type": "integer", "scope": "language-overridable", "description": "The number of non empty lines from a diagnostic to display", "default": 3 }, "diagnostic.virtualTextPrefix": { "type": "string", "scope": "language-overridable", "description": "The prefix added virtual text diagnostics", "default": " " }, "diagnostic.virtualTextWinCol": { "type": ["integer", "null"], "scope": "language-overridable", "description": "Window column number to align virtual text, neovim only.", "default": null }, "diagnostic.warningSign": { "type": "string", "scope": "application", "description": "Text of warning sign", "default": "⚠" }, "diagnostic.showRelatedInformation": { "type": "boolean", "default": true, "scope": "language-overridable", "description": "Display related information in the diagnostic floating window." }, "dialog.confirmKey": { "type": "string", "default": "", "scope": "application", "description": "Confirm key for confirm selection used by menu and picker, you can always use to cancel." }, "dialog.floatBorderHighlight": { "type": ["string", "null"], "default": null, "scope": "application", "description": "Highlight group for border of dialog window/popup, default to 'CocFloatBorder'" }, "dialog.floatHighlight": { "type": ["string", "null"], "default": null, "scope": "application", "description": "Highlight group for dialog window/popup, default to 'CocFloating'" }, "dialog.maxHeight": { "type": "integer", "default": 30, "scope": "application", "description": "Maximum height of dialog window." }, "dialog.maxWidth": { "type": "integer", "default": 80, "scope": "application", "description": "Maximum width of dialog window." }, "dialog.pickerButtonShortcut": { "type": "boolean", "default": true, "scope": "application", "description": "Show shortcut in buttons of picker dialog window/popup, used when dialog.pickerButtons is true." }, "dialog.pickerButtons": { "type": "boolean", "default": true, "scope": "application", "description": "Show buttons for picker dialog window/popup." }, "dialog.rounded": { "type": "boolean", "default": true, "scope": "application", "description": "use rounded border for dialog window." }, "dialog.shortcutHighlight": { "type": "string", "default": "MoreMsg", "scope": "application", "description": "Highlight group for shortcut character in menu dialog, default to 'MoreMsg'" }, "documentHighlight.priority": { "type": "integer", "default": -1, "scope": "resource", "description": "Match priority used by document highlight, see ':h matchadd'." }, "documentHighlight.limit": { "type": "integer", "default": 200, "scope": "resource", "description": "Limit the highlights added by matchaddpos, too many positions could cause vim slow." }, "documentHighlight.timeout": { "type": "integer", "default": 300, "minimum": 200, "maximum": 5000, "scope": "resource", "description": "Timeout for document highlight, in milliseconds." }, "editor.autocmdTimeout": { "type": "integer", "default": 1000, "minimum": 100, "maximum": 5000, "scope": "application", "description": "Timeout for execute request autocmd registered by coc extensions." }, "editor.codeActionsOnSave": { "additionalProperties": { "type": ["string", "boolean"], "enum": ["always", "never", true, false] }, "type": "object", "default": {}, "scope": "language-overridable", "description": "Run Code Actions for the buffer on save, normally source actions, Example: `\"source.organizeImports\": \"always\"" }, "fileSystemWatch.watchmanPath": { "type": ["null", "string"], "scope": "application", "description": "executable path for https://facebook.github.io/watchman/, detected from $PATH by default", "default": null }, "fileSystemWatch.enable": { "type": "boolean", "default": true, "scope": "application", "description": "Enable file system watch support for workspace folders." }, "fileSystemWatch.ignoredFolders": { "type": "array", "default": ["/private/tmp", "/", "${tmpdir}"], "scope": "application", "description": "List of folders that should not be watched for file changes, environment variables and minimatch patterns can be used.", "items": { "type": "string" } }, "floatFactory.floatConfig": { "type": "object", "scope": "application", "description": "Configure default style float window/popup created by float factory (created around cursor and automatically closed)", "allOf": [{"$ref": "#/definitions/floatConfig"}], "additionalProperties": false, "properties": { "border": {}, "rounded": {}, "highlight": {}, "borderhighlight": {}, "title": {}, "close": {}, "maxWidth": {}, "maxHeight": {}, "winblend": {}, "focusable": {}, "shadow": {}, "position": {}, "top": {}, "bottom": {}, "left": {}, "right": {} } }, "hover.autoHide": { "type": "boolean", "scope": "application", "default": true, "description": "Automatically hide hover float window on CursorMove or InsertEnter." }, "hover.floatConfig": { "type": "object", "scope": "application", "description": "Configure float window style of hover documents.", "allOf": [{"$ref": "#/definitions/floatConfig"}], "additionalProperties": false, "properties": { "border": {}, "rounded": {}, "highlight": {}, "borderhighlight": {}, "title": {}, "close": {}, "maxHeight": {}, "maxWidth": {}, "winblend": {}, "focusable": {}, "shadow": {}, "position": {}, "top": {}, "bottom": {}, "left": {}, "right": {} } }, "hover.previewMaxHeight": { "type": "integer", "scope": "resource", "default": 12, "description": "Max height of preview window for hover." }, "hover.target": { "type": "string", "default": "float", "scope": "resource", "description": "Target to show hover information, default is floating window when possible.", "enum": ["preview", "echo", "float"] }, "http.proxy": { "type": "string", "default": "", "pattern": "^https?://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$", "description": "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables.", "scope": "application" }, "http.proxyAuthorization": { "type": ["null", "string"], "description": "The value to send as the `Proxy-Authorization` header for every network request.", "default": null, "scope": "application" }, "http.proxyCA": { "type": "string", "description": "CA (file) to use as Certificate Authority", "default": null, "scope": "application" }, "http.proxyStrictSSL": { "type": "boolean", "description": "Controls whether the proxy server certificate should be verified against the list of supplied CAs", "default": true, "scope": "application" }, "extensions.updateCheck": { "type": "string", "scope": "application", "default": "never", "description": "Interval time for check extension update, could be daily, weekly, never", "enum": ["daily", "weekly", "never"] }, "extensions.recommendations": { "type": "array", "scope": "resource", "description": "List of extensions recommended for installation in the current project", "default": [], "items": { "type": "string" } }, "extensions.silentAutoupdate": { "type": "boolean", "description": "Not open split window with update status when performing auto update.", "scope": "application", "default": true }, "extensions.updateUIInTab": { "type": "boolean", "scope": "application", "default": false, "description": "Display extension updating UI in new vim tab" }, "inlayHint.enable": { "type": "boolean", "default": true, "scope": "language-overridable", "description": "Enable inlay hint support" }, "inlayHint.position": { "type": "string", "default": "inline", "scope": "language-overridable", "description": "Controls where to show inlay hints: inline in the text, or at the end of the line", "enum": ["inline", "eol"] }, "inlayHint.enableParameter": { "type": "boolean", "scope": "language-overridable", "default": true, "description": "Enable inlay hints for parameters." }, "inlayHint.display": { "type": "boolean", "scope": "language-overridable", "default": true, "description": "Display inlay hints." }, "inlayHint.filetypes": { "type": ["array", "null"], "scope": "application", "description": "Filetypes that enable inlayHint, all filetypes are enabled by default", "deprecationMessage": "Use inlayHint.enable with language scope instead, see :h coc-configuration-scope", "default": null, "items": { "type": "string" } }, "inlayHint.refreshOnInsertMode": { "type": "boolean", "default": false, "scope": "language-overridable", "description": "Refresh inlayHints on insert mode." }, "inlayHint.maximumLength": { "type": "integer", "default": 0, "minimum": 0, "scope": "language-overridable", "description": "Maximum overall length of inlay hints, for a single line, before they get truncated by the editor. Set to `0`to disable truncation." }, "links.enable": { "type": "boolean", "scope": "language-overridable", "description": "Enable document links", "default": true }, "links.tooltip": { "type": "boolean", "scope": "application", "description": "Show tooltip of link under cursor on CursorHold.", "default": false }, "links.highlight": { "type": "boolean", "scope": "application", "description": "Use CocLink highlight group to highlight links", "default": false }, "list.floatPreview": { "type": "boolean", "default": false, "scope": "application", "description": "Enable preview with float window/popup, default: `false`" }, "list.alignColumns": { "type": "boolean", "default": false, "scope": "application", "description": "Whether to align lists in columns, default: `false`" }, "list.extendedSearchMode": { "type": "boolean", "scope": "application", "default": true, "description": "Enable extended search mode which allows multiple search patterns delimited by spaces." }, "list.height": { "type": "integer", "scope": "application", "default": 10, "description": "Height of split list window." }, "list.indicator": { "type": "string", "default": ">", "scope": "application", "description": "The character used as first character in prompt line." }, "list.insertMappings": { "type": "object", "scope": "application", "default": {}, "description": "Custom keymappings on insert mode." }, "list.interactiveDebounceTime": { "type": "integer", "default": 100, "scope": "application", "description": "Debounce time for input change on interactive mode." }, "list.limitLines": { "type": ["number", "null"], "scope": "application", "default": null, "description": "Limit lines for list buffer." }, "list.maxPreviewHeight": { "type": "integer", "scope": "application", "default": 12, "description": "Max height for preview window of list." }, "list.menuAction": { "type": "boolean", "default": false, "scope": "application", "description": "Use menu picker instead of confirm() for choose action." }, "list.nextKeymap": { "type": "string", "scope": "application", "default": "", "description": "Key used for select next line on insert mode." }, "list.normalMappings": { "type": "object", "scope": "application", "default": {}, "description": "Custom keymappings on normal mode." }, "list.previewHighlightGroup": { "type": "string", "scope": "application", "default": "Search", "description": "Highlight group used for highlight the range in preview window." }, "list.previewSplitRight": { "type": "boolean", "scope": "application", "default": false, "description": "Use vsplit for preview window." }, "list.previewToplineOffset": { "type": "integer", "scope": "application", "default": 3, "description": "Topline offset for list previews" }, "list.previewToplineStyle": { "type": "string", "scope": "application", "default": "offset", "description": "Topline style for list previews", "enum": ["offset", "middle"] }, "list.previousKeymap": { "type": "string", "scope": "application", "default": "", "description": "Key used for select previous line on insert mode." }, "list.selectedSignText": { "type": "string", "scope": "application", "default": "*", "description": "Sign text for selected lines." }, "list.signOffset": { "type": "integer", "scope": "application", "default": 900, "description": "Sign offset of list, should be different from other plugins." }, "list.smartCase": { "type": "boolean", "default": false, "scope": "application", "description": "Use smartcase match for fuzzy match and strict match, --ignore-case will be ignored, may not affect interactive list." }, "list.source.diagnostics.includeCode": { "type": "boolean", "scope": "application", "description": "Whether to show the diagnostic code in the list.", "default": true }, "list.source.diagnostics.pathFormat": { "type": "string", "scope": "application", "description": "Decide how the filepath is shown in the list.", "enum": ["full", "short", "filename", "hidden"], "default": "full" }, "list.source.outline.ctagsFiletypes": { "type": "array", "scope": "application", "default": [], "description": "Filetypes that should use ctags for outline instead of language server.", "items": { "type": "string" } }, "list.source.symbols.excludes": { "type": "array", "scope": "application", "default": [], "description": "Patterns of minimatch for filepath to exclude from symbols list.", "items": { "type": "string" } }, "list.statusLineSegments": { "type": ["array", "null"], "scope": "application", "default": [ "%#CocListMode#-- %{coc#list#status(\"mode\")} --%*", "%{coc#list#status(\"loading\")}", "%{coc#list#status(\"args\")}", "(%L/%{coc#list#status(\"total\")})", "%=", "%#CocListPath# %{coc#list#status(\"cwd\")} %l/%L%*" ], "items": { "types": "string" }, "description": "An array of statusline segments that will be used to draw the status line for list windows." }, "notification.statusLineProgress": { "type": "boolean", "default": true, "scope": "application", "description": "Show progress notification in status line, instead of float window/popup." }, "notification.border": { "type": "boolean", "default": true, "scope": "application", "description": "Enable rounded border for notification windows." }, "notification.disabledProgressSources": { "type": "array", "default": [], "scope": "application", "description": "Sources that should be disabled for message progress, use * to disable all message only progresses", "items": { "type": "string" } }, "notification.focusable": { "type": "boolean", "default": true, "scope": "application", "description": "Enable focus by user actions (wincmds, mouse events), neovim only." }, "notification.highlightGroup": { "type": "string", "default": "Normal", "scope": "application", "description": "Highlight group of notification dialog." }, "notification.marginRight": { "type": "integer", "default": 10, "scope": "application", "description": "Margin right to the right of editor window." }, "notification.maxHeight": { "type": "integer", "default": 10, "scope": "application", "description": "Maximum content height of notification dialog." }, "notification.maxWidth": { "type": "integer", "default": 60, "scope": "application", "description": "Maximum content width of notification dialog." }, "notification.minProgressWidth": { "type": "integer", "default": 30, "scope": "application", "description": "Minimal with of progress notification." }, "notification.timeout": { "type": "integer", "default": 10000, "scope": "application", "description": "Timeout for auto close notifications, in milliseconds." }, "notification.winblend": { "type": "integer", "default": 30, "minimum": 0, "maximum": 100, "scope": "application", "description": "Winblend option of notification window, neovim only." }, "npm.binPath": { "type": "string", "scope": "application", "default": "npm", "description": "Command or absolute path to npm or yarn." }, "outline.autoHide": { "type": "boolean", "scope": "application", "default": false, "description": "Automatically close the outline window when an item is clicked." }, "outline.autoPreview": { "type": "boolean", "scope": "application", "default": false, "description": "Enable auto preview on cursor move." }, "outline.autoWidth": { "type": "boolean", "scope": "application", "default": true, "description": "Automatically increase window width to avoid wrapped lines." }, "outline.checkBufferSwitch": { "type": "boolean", "scope": "application", "default": true, "description": "Recreate outline view after user changed to another buffer on current tab." }, "outline.codeActionKinds": { "type": "array", "scope": "application", "default": ["", "quickfix", "refactor"], "description": "Filter code actions in actions menu by kinds.", "items": { "type": "string", "enum": ["", "quickfix", "refactor", "source"] } }, "outline.detailAsDescription": { "type": "boolean", "scope": "application", "default": true, "description": "Show detail as description aside with label, when false detail will be shown in tooltip on cursor hold." }, "outline.expandLevel": { "type": "integer", "scope": "application", "default": 1, "description": "Expand level of tree nodes." }, "outline.followCursor": { "type": "boolean", "scope": "application", "default": true, "description": "Reveal item in outline tree on cursor hold." }, "outline.keepWindow": { "type": "boolean", "scope": "application", "default": false, "description": "Jump back to original window after outline is shown." }, "outline.previewBorder": { "type": "boolean", "scope": "application", "default": true, "description": "Use border for preview window." }, "outline.previewBorderHighlightGroup": { "type": "string", "scope": "application", "default": "Normal", "description": "Border highlight group of preview window." }, "outline.previewBorderRounded": { "type": "boolean", "scope": "application", "default": false, "description": "Use rounded border for preview window." }, "outline.previewHighlightGroup": { "type": "string", "scope": "application", "default": "Normal", "description": "Highlight group of preview window." }, "outline.previewMaxWidth": { "type": "integer", "scope": "application", "default": 80, "description": "Max width of preview window." }, "outline.previewWinblend": { "type": "integer", "scope": "application", "default": 0, "minimum": 0, "maximum": 100, "description": "Enables pseudo-transparency by set 'winblend' option of window, neovim only." }, "outline.showLineNumber": { "type": "boolean", "scope": "application", "default": true, "description": "Show line number of symbols." }, "outline.sortBy": { "type": "string", "scope": "application", "default": "category", "description": "Sort method for symbols.", "enum": ["position", "name", "category"] }, "outline.splitCommand": { "type": "string", "scope": "application", "default": "botright 30vs", "description": "Window split command used by outline." }, "outline.switchSortKey": { "type": "string", "scope": "application", "default": "", "description": "The key used to switch sort method for symbols provider of current tree view." }, "outline.togglePreviewKey": { "type": "string", "scope": "application", "default": "p", "description": "The key used to toggle auto preview feature." }, "pullDiagnostic.ignored": { "type": "array", "default": [], "scope": "application", "description": "Minimatch patterns to match full filepath that should be ignored for pullDiagnostic.", "items": { "type": "string" } }, "pullDiagnostic.onChange": { "type": "boolean", "default": true, "scope": "language-overridable", "description": "Whether to pull for diagnostics on document change." }, "pullDiagnostic.onSave": { "type": "boolean", "default": false, "scope": "language-overridable", "description": "Whether to pull for diagnostics on document save." }, "pullDiagnostic.workspace": { "type": "boolean", "default": true, "scope": "application", "description": "Whether to pull for workspace diagnostics when possible." }, "refactor.afterContext": { "type": "integer", "scope": "application", "default": 3, "description": "Print num lines of trailing context after each match." }, "refactor.beforeContext": { "type": "integer", "scope": "application", "default": 3, "description": "Print num lines of leading context before each match." }, "refactor.openCommand": { "type": "string", "scope": "application", "description": "Open command for refactor window.", "default": "vsplit" }, "refactor.saveToFile": { "type": "boolean", "scope": "application", "description": "Save changed buffer to file when write refactor buffer with ':noa wa' command.", "default": true }, "refactor.showMenu": { "type": "string", "scope": "application", "default": "", "description": "Refactor buffer local mapping to bring up menu for this chunk." }, "semanticTokens.combinedModifiers": { "type": "array", "scope": "language-overridable", "description": "Semantic token modifiers that should have highlight combined with syntax highlights.", "default": ["deprecated"], "items": { "type": "string" } }, "semanticTokens.enable": { "type": "boolean", "default": false, "scope": "language-overridable", "description": "Enable semantic tokens support" }, "semanticTokens.filetypes": { "type": ["array", "null"], "scope": "resource", "description": "Filetypes that enable semantic tokens highlighting or [\"*\"] for any filetype", "deprecationMessage": "Use semanticTokens.enable configuration with language scope instead, see :h coc-configuration-scope", "default": null, "items": { "type": "string" } }, "semanticTokens.highlightPriority": { "type": "integer", "scope": "language-overridable", "description": "Priority for semantic tokens highlight.", "default": 2048, "maximum": 4096 }, "semanticTokens.incrementTypes": { "type": "array", "scope": "language-overridable", "description": "Semantic token types that should increase highlight when insert at the start and end position of token.", "default": ["variable", "string", "parameter"], "items": { "type": "string" } }, "signature.enable": { "type": "boolean", "scope": "language-overridable", "description": "Enable show signature help when trigger character typed.", "default": true }, "signature.floatConfig": { "type": "object", "scope": "application", "description": "Configure float window style of signature documents.", "allOf": [{"$ref": "#/definitions/floatConfig"}], "additionalProperties": false, "properties": { "border": {}, "rounded": {}, "highlight": {}, "borderhighlight": {}, "title": {}, "close": {}, "maxHeight": {}, "maxWidth": {}, "winblend": {}, "focusable": {}, "shadow": {}, "position": {}, "top": {}, "bottom": {}, "left": {}, "right": {} } }, "signature.hideOnTextChange": { "type": "boolean", "scope": "language-overridable", "description": "Hide signature float window when text changed on insert mode.", "default": false }, "signature.preferShownAbove": { "type": "boolean", "scope": "application", "description": "Show signature help float window above cursor when possible, require restart service on change.", "default": true }, "signature.target": { "type": "string", "scope": "language-overridable", "description": "Target of signature help, use float when possible by default.", "default": "float", "enum": ["float", "echo"] }, "signature.triggerSignatureWait": { "type": "integer", "scope": "language-overridable", "default": 500, "minimum": 200, "maximum": 1000, "description": "Timeout for trigger signature help, in milliseconds." }, "snippet.highlight": { "type": "boolean", "scope": "application", "description": "Use highlight group 'CocSnippetVisual' to highlight placeholders with same index of current one.", "default": false }, "snippet.nextPlaceholderOnDelete": { "type": "boolean", "scope": "application", "description": "Automatically jump to the next placeholder when the current one is completely deleted.", "default": false }, "snippet.statusText": { "type": "string", "scope": "application", "default": "SNIP", "description": "Text shown in statusline to indicate snippet session is activated." }, "suggest.acceptSuggestionOnCommitCharacter": { "type": "boolean", "default": false, "scope": "language-overridable", "description": "Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character." }, "suggest.asciiCharactersOnly": { "type": "boolean", "description": "Trigger suggest with ASCII characters only", "scope": "language-overridable", "default": false }, "suggest.segmenterLocales": { "type": ["string", "null"], "default": "", "scope": "language-overridable", "description": "Locales used for divide sentence into segments for around and buffer source, works when NodeJS built with intl support, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#parameters, default empty string means auto detect, use null to disable this feature." }, "suggest.asciiMatch": { "type": "boolean", "description": "Convert unicode characters to ascii for match", "scope": "language-overridable", "default": true }, "suggest.autoTrigger": { "type": "string", "scope": "language-overridable", "default": "always", "description": "How should completion be triggered", "enum": ["always", "trigger", "none"] }, "suggest.reTriggerAfterIndent": { "type": "boolean", "description": "Re-Trigger completion after indent changes", "scope": "language-overridable", "default": true }, "suggest.completionItemKindLabels": { "type": "object", "default": {}, "scope": "application", "description": "Set custom labels to completion items' kinds.", "properties": { "text": {"type": "string"}, "method": {"type": "string"}, "function": {"type": "string"}, "constructor": {"type": "string"}, "field": {"type": "string"}, "variable": {"type": "string"}, "class": {"type": "string"}, "interface": {"type": "string"}, "module": {"type": "string"}, "property": {"type": "string"}, "unit": {"type": "string"}, "value": {"type": "string"}, "enum": {"type": "string"}, "keyword": {"type": "string"}, "snippet": {"type": "string"}, "color": {"type": "string"}, "file": {"type": "string"}, "reference": {"type": "string"}, "folder": {"type": "string"}, "enumMember": {"type": "string"}, "constant": {"type": "string"}, "struct": {"type": "string"}, "event": {"type": "string"}, "operator": {"type": "string"}, "typeParameter": {"type": "string"}, "default": {"type": "string"} }, "additionalProperties": false }, "suggest.defaultSortMethod": { "type": "string", "description": "Default sorting behavior for suggested completion items.", "default": "length", "scope": "language-overridable", "enum": ["length", "alphabetical", "none"] }, "suggest.detailField": { "type": "string", "scope": "application", "default": "preview", "description": "Where to show the detail text of CompleteItem from LS.", "enum": ["abbr", "preview"] }, "suggest.detailMaxLength": { "type": "integer", "scope": "application", "description": "Max length of detail that should be shown in popup menu.", "deprecationMessage": "Use suggest.labelMaxLength instead.", "default": 100 }, "suggest.enableFloat": { "type": "boolean", "scope": "language-overridable", "description": "Enable float window with documentation aside with popupmenu.", "default": true }, "suggest.enablePreselect": { "type": "boolean", "scope": "application", "description": "Enable preselect feature of LSP, works when suggest.noselect is false.", "default": true }, "suggest.filterGraceful": { "type": "boolean", "description": "Controls whether filtering and sorting suggestions accounts for small typos.", "scope": "language-overridable", "default": true }, "suggest.filterOnBackspace": { "type": "boolean", "scope": "application", "description": "Filter complete items on backspace.", "default": true }, "suggest.floatConfig": { "type": "object", "scope": "application", "description": "Configure style of popup menu and documentation window of completion.", "allOf": [{"$ref": "#/definitions/floatConfig"}], "additionalProperties": false, "properties": { "border": {}, "rounded": {}, "highlight": {}, "borderhighlight": {}, "title": {}, "maxWidth": {}, "winblend": {}, "shadow": {} } }, "suggest.formatItems": { "type": "array", "scope": "application", "items": { "enum": ["abbr", "menu", "kind", "shortcut"] }, "contains": { "enum": ["abbr"] }, "uniqueItems": true, "description": "Items shown in popup menu in order.", "default": ["abbr", "menu", "kind", "shortcut"] }, "suggest.highPrioritySourceLimit": { "type": "integer", "minimum": 1, "maximum": 100, "scope": "language-overridable", "description": "Max items count for source priority bigger than or equal to 90." }, "suggest.insertMode": { "type": "string", "scope": "language-overridable", "default": "replace", "description": "Controls whether words are overwritten when accepting completions.", "enum": ["insert", "replace"] }, "suggest.ignoreRegexps": { "type": "array", "scope": "language-overridable", "items": { "type": "string" }, "description": "Regexps to ignore when trigger suggest", "default": [] }, "suggest.invalidInsertCharacters": { "type": "array", "items": { "type": "string" }, "scope": "application", "description": "Invalid character for strip valid word when inserting text of complete item.", "default": ["\r", "\n"] }, "suggest.labelMaxLength": { "type": "integer", "scope": "application", "description": "Max length of abbr that shown as label of complete item.", "default": 200 }, "suggest.languageSourcePriority": { "type": "integer", "default": 99, "scope": "language-overridable", "description": "Priority of language sources." }, "suggest.localityBonus": { "type": "boolean", "description": "Controls whether sorting favors words that appear close to the cursor.", "scope": "language-overridable", "default": true }, "suggest.lowPrioritySourceLimit": { "type": "integer", "minimum": 1, "maximum": 100, "scope": "language-overridable", "description": "Max items count for source priority lower than 90." }, "suggest.maxCompleteItemCount": { "type": "integer", "default": 256, "scope": "language-overridable", "description": "Maximum number of complete items shown in vim" }, "suggest.minTriggerInputLength": { "type": "integer", "default": 1, "scope": "language-overridable", "description": "Minimal input length for trigger completion, default 1" }, "suggest.noselect": { "type": "boolean", "scope": "application", "description": "Not make vim select first item on popupmenu shown", "default": false }, "suggest.preferCompleteThanJumpPlaceholder": { "type": "boolean", "description": "Confirm completion instead of jump to next placeholder when completion is activated.", "scope": "resource", "default": false }, "suggest.pumFloatConfig": { "type": ["object", "null"], "scope": "application", "description": "Configure style of popup menu, suggest.floatConfig is used when not specified.", "allOf": [{"$ref": "#/definitions/floatConfig"}], "additionalProperties": false, "default": null, "properties": { "title": {}, "border": {}, "rounded": {}, "highlight": {}, "borderhighlight": {}, "winblend": {}, "shadow": {} } }, "suggest.removeDuplicateItems": { "type": "boolean", "description": "Remove completion items with duplicated word for all sources, snippet items are excluded.", "scope": "language-overridable", "default": false }, "suggest.removeCurrentWord": { "type": "boolean", "description": "Remove word item (from around and buffer source) that is identical to current input", "scope": "language-overridable", "default": false }, "suggest.reversePumAboveCursor": { "type": "boolean", "scope": "application", "description": "Reverse order of complete items when pum shown above cursor.", "default": false }, "suggest.selection": { "type": "string", "scope": "application", "default": "first", "description": "Controls how suggestions are pre-selected when showing the suggest list.", "enum": ["first", "recentlyUsed", "recentlyUsedByPrefix"] }, "suggest.snippetIndicator": { "type": "string", "default": "~", "scope": "application", "description": "The character used in abbr of complete item to indicate the item could be expand as snippet." }, "suggest.snippetsSupport": { "type": "boolean", "scope": "language-overridable", "description": "Set to false to disable snippets support of completion.", "default": true }, "suggest.timeout": { "type": "integer", "default": 5000, "minimum": 500, "maximum": 15000, "scope": "language-overridable", "description": "Timeout for completion, in milliseconds." }, "suggest.triggerAfterInsertEnter": { "type": "boolean", "description": "Trigger completion after InsertEnter, auto trigger should be 'always' to enable this option", "scope": "language-overridable", "default": false }, "suggest.triggerCompletionWait": { "type": "integer", "default": 0, "minimum": 0, "maximum": 10, "scope": "language-overridable", "description": "Wait time between text change and completion start, completion is canceled when text changed during wait." }, "suggest.virtualText": { "type": "boolean", "scope": "application", "description": "Show virtual text for insert word of the selected item if any", "default": false }, "inlineSuggest.autoTrigger": { "type": "boolean", "scope": "language-overridable", "description": "Enable automatically trigger inline completion on insert mode cursor hold.", "default": true }, "inlineSuggest.triggerCompletionWait": { "type": "integer", "default": 0, "minimum": 0, "maximum": 1000, "scope": "language-overridable", "description": "Wait time in milliseconds between text change and trigger inline completion." }, "tree.closedIcon": { "type": "string", "scope": "application", "default": "+", "description": "Closed icon of tree view." }, "tree.key.actions": { "type": "string", "scope": "application", "default": "", "description": "Trigger key to invoke actions." }, "tree.key.activeFilter": { "type": "string", "scope": "application", "default": "f", "description": "Trigger key active filter." }, "tree.key.close": { "type": "string", "scope": "application", "default": "", "description": "Trigger key to dispose the tree and close tree window." }, "tree.key.collapseAll": { "type": "string", "scope": "application", "default": "M", "description": "Trigger key to collapse all tree node." }, "tree.key.invoke": { "type": "string", "scope": "application", "default": "", "description": "Trigger key to invoke default command of current node or selection." }, "tree.key.selectNext": { "type": "string", "scope": "application", "default": "", "description": "Trigger key to select next item during filter." }, "tree.key.selectPrevious": { "type": "string", "scope": "application", "default": "", "description": "Trigger key to select previous item during filter." }, "tree.key.toggle": { "type": "string", "scope": "application", "default": "t", "description": "Trigger key to toggle expand state of tree node, does nothing with leaf node." }, "tree.key.toggleSelection": { "type": "string", "scope": "application", "default": "", "description": "Trigger key to select/unselect item" }, "tree.openedIcon": { "type": "string", "scope": "application", "default": "-", "description": "Opened icon of tree view." }, "typeHierarchy.enableTooltip": { "type": "boolean", "scope": "application", "default": true, "description": "Enable tooltip to show relative filepath of type hierarchy." }, "typeHierarchy.openCommand": { "type": "string", "scope": "application", "default": "edit", "description": "Open command for type hierarchy tree view." }, "typeHierarchy.splitCommand": { "type": "string", "scope": "application", "default": "botright 30vs", "description": "Window split command used by type hierarchy tree view." }, "workspace.rootPatterns": { "type": "array", "default": [".git", ".hg", ".projections.json"], "scope": "application", "description": "Root patterns to resolve workspaceFolder from parent folders of opened files, resolved from up to down.", "items": { "type": "string" } }, "workspace.bottomUpFiletypes": { "type": "array", "default": [], "scope": "application", "description": "Filetypes that should have workspace folder should resolved from base directory of file, or [\"*\"] for any filetype.", "items": { "type": "string" } }, "workspace.ignoredFiletypes": { "type": "array", "default": [], "scope": "application", "description": "Filetypes that should be ignored for workspace folder resolve.", "items": { "type": "string" } }, "workspace.ignoredFolders": { "type": "array", "default": ["$HOME", "$HOME/.cargo/**", "$HOME/.rustup/**", "$HOME/pkg/mod/**", "$HOMEBREW_PREFIX/**"], "scope": "application", "description": "List of folders that should not be resolved as workspace folder, environment variables and minimatch patterns can be used.", "items": { "type": "string" } }, "workspace.openOutputCommand": { "type": "string", "default": "vs", "scope": "resource", "description": "Command used to open output channel." }, "workspace.openResourceCommand": { "type": "string", "default": "tab drop", "scope": "application", "description": "Command to open files that not loaded, load files as hidden buffers when empty." }, "workspace.workspaceFolderCheckCwd": { "type": "boolean", "default": true, "scope": "application", "description": "Whether the current working directory should be used first when checking patterns match for workspace folder." }, "workspace.workspaceFolderFallbackCwd": { "type": "boolean", "default": true, "scope": "application", "description": "Use current working directory as workspace folder when no root patterns resolved." }, "workspace.removeEmptyWorkspaceFolder": { "type": "boolean", "default": false, "scope": "application", "description": "Automatically remove the workspace folder when no buffer associated with it." }, "languageserver": { "type": "object", "default": {}, "scope": "resource", "description": "Dictionary of languageservers, key is used as id of languageserver, restart coc.nvim required after change.", "patternProperties": { "^[_a-zA-Z]+$": { "oneOf": [ { "$ref": "#/definitions/languageServerModule" }, { "$ref": "#/definitions/languageServerCommand" }, { "$ref": "#/definitions/languageServerSocket" } ] } } } } } ================================================ FILE: doc/coc-api.txt ================================================ *coc-api.txt* NodeJS client for Vim & Neovim. CONTENTS Vim sources |coc-api-vim-source| Extension introduction |coc-api-intro| Extension package json |coc-api-json| Single file extensions |coc-api-single| Create custom Extensions |coc-api-extension| Debug extensions |coc-api-debug| ============================================================================== The guide for extend coc.nvim by create vim completion sources and coc.nvim extensions. ------------------------------------------------------------------------------ VIM SOURCES *coc-api-vim-source* During initialization, coc.nvim searches vim's |runtimepath| for file pattern `autoload/coc/source/${name}.vim`, matched files would be loaded as vim completion sources. Note: LSP completion features like `TextEdit`, `additionalTextEdits`, `command` are not supported by vim sources, use the NodeJS API `languages.registerCompletionItemProvider` for LSP completion. For example, create a file `autoload/coc/source/email.vim` inside your plugin folder. With code: > " vim source for emails function! coc#source#email#init() abort return { \ 'priority': 9, \ 'shortcut': 'Email', \ 'triggerCharacters': ['@'] \} endfunction function! coc#source#email#complete(option, cb) abort let items = ['foo@gmail.com', 'bar@yahoo.com'] call a:cb(items) endfunction < `init` and `complete` are required functions for vim sources, error message will be shown when not exists. vim9script can be also used on vim9 (not supported on neovim), the function first letter need to be uppercased, like: > vim9script export def Init(): dict return { priority: 9, shortcut: 'Email', triggerCharacters: ['@'] } enddef export def Complete(option: dict, Callback: func(list)) const items = ['foo@gmail.com', 'bar@yahoo.com'] Callback(items) enddef < Source option: ~ The source option object is returned by `coc#source#{name}#init` function, available properties: • shortcut: The shortcut characters shown in popup menu, first three characters from the source name would be used when not exists. • priority: The priority of source, default to `9`. • filetypes: Array of filetype names this source should be triggered by. Available for all filetypes when not exists. • firstMatch: When is truthy value, only the completion item that has the first letter matching the user input will be shown. • triggerCharacters: Trigger characters for this source, default to `[]`. • triggerOnly: The source should only be triggered by trigger characters, when trigger characters is false or empty, the source would only be triggered by api |coc#start()|. • isSnippet: All complete items returned by `complete` are snippets, which would have snippet indicator text added to the label in popup menu. The "isSnippet" property of completion item override this option. All options are optional. Source configurations: ~ Vim sources register |coc-configuration| for allow the user to customize the source behavior. • `coc.source.${name}.enable` Enable the source, default to `true`. • `coc.source.${name}.disableSyntaxes` Disabled syntax names when trigger completion. • `coc.source.${name}.firstMatch` Default to "firstMatch" of source option. • `coc.source.${name}.priority` Default to "priority" of source option. • `coc.source.${name}.shortcut` Default to "shortcut" of source option. • `coc.source.${name}.filetypes` Default to "filetypes" of source option. Complete function: ~ The complete function is called with complete option as the first argument and a callback function as the second argument, the callback function should be called with list of complete item or `v:null` synchronously or asynchronously. Note: synchronously compute complete items blocks vim's operation. Note: Error during completion is not thrown, use |:CocOpenLog| to check the error log. Complete option have following properties: • bufnr: Current buffer number. • line: Content line when trigger completion. • col: Start col of completion, start col of the keywords before cursor by default, 0 based. • input: Input text between start col and cursor col. • filetype: Filetype of current buffer. • filepath: Fullpath of current buffer. • changedtick: b:changedtick value when trigger completion. • triggerCharacter: The character which trigger the completion, could be empty string. • colnr: Cursor col when trigger completion, 1 based. • linenr: Line number of cursor, 1 based. Complete items extends vim's |complete-items| with the following properties: • deprecated: The complete item would be rendered with strike through highlight when truthy. • labelDetails: Additional details for a completion item label, which have optional `detail` and/or `description` text. • sortText: A string that should be used when comparing this item with other items, word is used when not exists. • filterText: A string that should be used when filtering a set of complete items, word is used when not exists. • insertText: The text to insert, could be textmate snippet text, word is used when not exists. • isSnippet: The text to insert is snippet when is truthy value, when truthy and `on_complete` not provided by vim source, the `insertText` is expanded as textmate snippet when confirm completion. • documentation: Array of `Documentation`, which provide `filetype` and `content` text to be displayed in preview window. Only the "word" property is mandatory for complete items. Optional functions: ~ The vim source could provide some optional functions which would be invoked by coc.nvim: • `coc#source#{name}#get_startcol(option)` Used to alter the start col of completion, the returned col must <= current cursor col. • `coc#source#{name}#on_complete(item)` Called with selected complete item when user confirm the completion by |coc#pum#confirm()| or |coc#pum#select_confirm()|. Normally used for apply necessary edits to the buffer. • `coc#source#{name}#on_enter(option)` Called on |BufEnter| with option contains: • bufnr: The buffer number. • uri: The uri text of buffer. • languageId: The mapped filetype of buffer, see |coc-document-filetype|. • `coc#source#{name}#refresh()` Called when the user trigger refresh action for the source. ------------------------------------------------------------------------------ EXTENSION INTRODUCTION *coc-api-intro* Every extension of coc.nvim has a JavaScript entry file, that file is loaded by NodeJS API `vm.runInContext` with an identical global context (like iframe in browser). The JavaScript entry file should be a CommonJS module with `activate` method exported, and `require('coc.nvim')` can be used to access modules exported by coc.nvim, for example: > const {window} = require('coc.nvim') exports.activate = async context => { window.showInformationMessage('extension activated') } < When `exports.deactivate` is exported from the JavaScript entry file as a function, it would be called on extension deactivate. Limitation of extension context: ~ Some methods/properties provided by NodeJS can't be used inside extension context, including: • `process.reallyExit()` • `process.abort()` • `process.setuid()` • `process.setgid()` • `process.setgroups()` • `process._fatalException()` • `process.exit()` • `process.kill()` • `process.umask()` Could only be used to get umask value. • `process.chdir()` Could be called, but no effect at all. Some globals may can't be accessed directly, for example `TextDecoder`, `TextEncoder`, use `globalThis` like `globalThis.TextDecoder` to access them. *coc-api-console* Stdin and stdout of the NodeJS process is used for communication between vim and NodeJS process, use the methods related to `process.stdin` and `process.stdout` may cause unexpected behavior. However, some methods of `console` are provided for debugging purpose. Messages from `console` of extension would be redirected to the log file |:CocOpenLog|. Available methods: • `debug(...args: any[])` Write debug message to the log file. • `log(...args: any[])` Write info message to the log file. • `info(...args: any[])` Write info message to the log file. • `error(...args: any[])` Write error message to the log file. • `warn(...args: any[])` Write warning message to the log file. Check the full NodeJS API interfaces at: https://github.com/neoclide/coc.nvim/blob/master/typings/index.d.ts ------------------------------------------------------------------------------ EXTENSION PACKAGE JSON *coc-api-json* The package.json file inside extension root defines the meta data of the extension. For example: > { "name": "coc-my-extension", "version": "1.0.0", "main": "lib/index.js", "engines": { "coc": "^0.0.82" }, "activationEvents": [ "*", ], "contributes": { "rootPatterns": [{ "filetype": "myfiletype", "patterns": [ "project_root.json" ] }], "commands": [{ "title": "My command", "category": "myextension", "id": "myextension.myCommand" }], "configuration": { "type": "object", "properties": { "myextension.enable": { "type": "boolean", "default": true, "scope": "resource", "description": "Enable running of my extension." } } } } } < Required properties of package.json: • name: The unique name of extension, to publish the extension, the name should not be taken by exists packages at https://www.npmjs.com/ • version: The semver version of extension. • engines: Should have `coc` property with minimal required coc.nvim version. The `main` property contains the relative filepath of the javascript entry file, `index.js` would be used when not exists. The `activationEvents` property tell coc.nvim when to activate the extension, when the property not exists or `*` is included, the extension would be activated during coc.nvim initialize. Other possible events: • onLanguage: Activate the extension when document of specific languageId exists, ex: `"onLanguage:vim"` activate the extension when there's buffer with languageId as vim loaded. • onFileSystem: Activate the extension when document with custom schema loaded, ex: `"onFileSystem:fugitive"` activate the extension when there's buffer with schema `fugitive` loaded. • onCommand: activate the extension when specific command invoked by user, ex: `"onCommand:tsserver.reloadProjects"` • workspaceContains: activate the extension when the glob pattern match one of the file in current workspace folder, ex: `"workspaceContains:**/package.json"` Optional `contributes` property contains the meta data that contributed to coc.nvim, including: • rootPatterns: The patterns to resolve |coc-workspace-folders| for associated filetype. • commands: List of commands with `id` and `title` that can be invoked by |:CocCommand|. • configuration: Contains `properties` object or a list of configurations that each one provide `properties` objects which define the configuration properties contributed by this extension. The `contributes` property could also contains other properties that used by other extensions, for example: the `jsonValidation` property could be used by coc-json. It's recommended to install `coc-json` for json intellisense support. ------------------------------------------------------------------------------ SINGLE FILE EXTENSIONS *coc-api-single* The easiest way to access the NodeJS API is make use of single file extensions. All Javascript files that ends with `.js` inside the folder "coc-extensions" under |g:coc_config_home| are considered as coc extensions. The javascript files would be loaded during coc.nvim initialize by default. To contribute extension meta data, create file `${name}.json` aside with `${name}.js`, the json file works the same as package.json of extension |coc-api-json|, except that only `activationEvents` and `contributes` properties are used. Single file extensions can't be managed by extensions list. ------------------------------------------------------------------------------ CREATE CUSTOM EXTENSIONS *coc-api-extension* To make an extension installable by |:CocInstall|, the easiest way is make use of https://github.com/fannheyward/create-coc-extension. Simply run command > npm init coc-extension [extension-name] < or > yarn create coc-extension [extension-name] < in terminal and you will be prompted for create a javascript/typescript extension step by step. To manually create an extension, follow these step: • Create an empty folder and goto that folder. • Create the package.json file |coc-api-json|. • Create a javascript file with name `index.js` and write code. • Add the created folder to your vim's runtimepath by add `set runtimepath^=/path/to/folder` in your vimrc. Recommended steps: • Install types of NodeJS and coc.nvim by terminal command `npm install @types/node@latest coc.nvim` in extension folder. • Bundle the javascript files when using multiple node dependencies by esbuild to save the time of installation. A typical build script looks like: > async function start() { await require('esbuild').build({ entryPoints: ['src/index.ts'], bundle: true, minify: process.env.NODE_ENV === 'production', sourcemap: process.env.NODE_ENV === 'development', mainFields: ['module', 'main'], external: ['coc.nvim'], platform: 'node', target: 'node16.18', outfile: 'lib/index.js' }) } start().catch(e => { console.error(e) }) < ------------------------------------------------------------------------------ DEBUG EXTENSIONS *coc-api-debug* *coc-api-channel* Channel errors: ~ Channel feature on vim9 is used by coc.nvim to communicate between vim and NodeJS, the error messages caused by channel commands are not displayed on the screen. Most of the time the error should be caught by coc.nvim and can be checked by |CocOpenLog|. But for some API functions including `callVim()` `exVim()` and `evalVim()`, the errors only update the |v:errmsg| and appears in vim's channel log, which can be checked by use |g:node_client_debug| or set environment variable `$COC_VIM_CHANNEL_ENABLE` to `"1"`. Uncaught errors: ~ When an uncaught error raised on the NodeJS process, the error message would be send to vim through stderr, and echoed by vim (unless |g:coc_disable_uncaught_error| is enabled). The error messages are not stored by vim's message history, use |:CocPrintErrors| to show previous errors. When error happens on the vim side, the promise would be rejected when sending request to vim, for notifications, vim would send `nvim_error_event` to the NodeJS process, and the node-client would create error log for it (could be opened by |:CocOpenLog|). Use the log file: ~ • Configure `NVIM_COC_LOG_LEVEL` to `trace` in vimrc: `let $NVIM_COC_LOG_LEVEL='trace'` • Configure `NVIM_COC_LOG_FILE` to a fixed in vimrc: `let $NVIM_COC_LOG_FILE=/tmp/coc.log`, otherwise it would be different for each vim instance. • Use |coc-api-console| to add console statements in javascript/typescript code and compile the extension when needed. • Tail the log file by `tail` command and make the issue happen. Add source map support: ~ When the javascript code is bundled by esbuild, it would be useful to have correct source map support for the error stack. • Install global source-map-support by `npm install -g source-map-support` • Find out the npm root by `npm root -g` • Load source-map-support with coc.nvim by append arguments to node in vimrc: `let g:coc_node_args = ['-r', '/path/to/npm/root/source-map-support/register']` Replace the part `/path/to/npm/root` with result from `npm root -g` terminal command. Note: the source-map-support module slows down the coc.nvim initialization. Debug javascript code with chrome: ~ • Add `let g:coc_node_args = ['--nolazy', '--inspect-brk=5858']` • Open vim and you will get the error message indicate that the debugger is listening. • Open Chrome browser with url chrome://inspect/#devices, configure the `Target discovery settings` and you will get the remote target to inspect. • Click the inspect link to open the devtools. • Click the sources label to debug javascript code. Other debugger clients can be used as well, see: https://nodejs.org/en/docs/guides/debugging-getting-started/ ============================================================================== vim:tw=78:sta:noet:ts=8:sts=0:ft=help:fen: ================================================ FILE: doc/coc-config.txt ================================================ *coc-config.txt* NodeJS client for Vim & Neovim. CONTENTS Core features Workspace |coc-config-workspace| File system watch |coc-config-fileSystemWatch| Extensions |coc-config-extensions| Preferences |coc-config-preferences| Editor |coc-config-editor| Float factory |coc-config-floatFactory| Float |coc-config-float| Tree |coc-config-tree| Dialog |coc-config-dialog| Http |coc-config-http| Npm |coc-config-npm| Language server |coc-config-languageserver| LSP features Call hierarchy |coc-config-callHierarchy| CodeLens |coc-config-codeLens| Colors |coc-config-colors| Completion |coc-config-suggest| Inline completion |coc-config-inlineSuggest| Cursors |coc-config-cursors| Diagnostics |coc-config-diagnostic| Document highlight |coc-config-documentHighlight| Hover |coc-config-hover| Inlay hint |coc-config-inlayHint| Links |coc-config-links| List |coc-config-list| Notification |coc-config-notification| Outline |coc-config-outline| Pull diagnostics |coc-config-pullDiagnostic| Refactor |coc-config-refactor| Semantic tokens |coc-config-semanticTokens| Signature |coc-config-signature| Type hierarchy |coc-config-typeHierarchy| ============================================================================== BUILTIN CONFIGURATIONS *coc-config* Builtin configurations of coc.nvim, it's recommended to use `coc-json` extension for completion and validation support. ============================================================================== CORE FEATURES Configurations of builtin features. ------------------------------------------------------------------------------ WORKSPACE *coc-config-workspace* "workspace.rootPatterns" *coc-config-workspace-rootPatterns* Root patterns to resolve workspaceFolder from parent folders of opened files, resolved from up to down. Scope: `application`, default: `[".git",".hg",".projections.json"]` "workspace.bottomUpFiletypes" *coc-config-workspace-bottomUpFiletypes* Filetypes that should have workspace folder should resolved from base directory of file, or ["*"] for any filetype. Scope: `application`, default: `[]` "workspace.ignoredFiletypes" *coc-config-workspace-ignoredFiletypes* Filetypes that should be ignored for workspace folder resolve. Scope: `resource`, default: `[]` "workspace.ignoredFolders" *coc-config-workspace-ignoredFolders* List of folders that should not be resolved as workspace folder, environment variables and minimatch patterns can be used. Scope: `application`, default: `["$HOME"]` "workspace.openOutputCommand" *coc-config-workspace-openOutputCommand* Command used to open output channel. Scope: `resource`, default: `"vs"` "workspace.openResourceCommand" *coc-config-workspace-openResourceCommand* Command to open files that not loaded, load files as hidden buffers when empty. Scope: `application`, default: `"tab drop"` "workspace.workspaceFolderCheckCwd" *coc-config-workspace-workspaceFolderCheckCwd* Whether the current working directory should be used first when checking patterns match for workspace folder. Scope: `application`, default: `true` "workspace.workspaceFolderFallbackCwd" *coc-config-workspace-workspaceFolderFallbackCwd* Use current working directory as workspace folder when no root patterns resolved. Scope: `application`, default: `true` "workspace.removeEmptyWorkspaceFolder" *coc-config-workspace-removeEmptyWorkspaceFolder* Automatically remove the workspace folder when no buffer associated with it. Scope: `application`, default: `false` ------------------------------------------------------------------------------ FILESYSTEMWATCH *coc-config-fileSystemWatch* "fileSystemWatch.watchmanPath" *coc-config-filesystemwatch-watchmanPath* executable path for https://facebook.github.io/watchman/, detected from $PATH by default Scope: `application`, default: `null` "fileSystemWatch.enable" *coc-config-filesystemwatch-enable* Enable file system watch support for workspace folders. Scope: `application`, default: `true` "fileSystemWatch.ignoredFolders" *coc-config-filesystemwatch-ignoredFolders* List of folders that should not be watched for file changes, environment variables and minimatch patterns can be used. Scope: `application`, default: `["/private/tmp", "/", "${tmpdir}"]` ------------------------------------------------------------------------------ EXTENSIONS *coc-config-extensions* "extensions.updateCheck" *coc-config-extensions-updateCheck* Interval for check extension update, could be "daily", "weekly" or "never" Scope: `application`, default: `"never"` "extensions.silentAutoupdate" *coc-config-extensions-silentAutoupdate* Not open split window with update status when performing auto update. Scope: `application`, default: `true` "extensions.updateUIInTab" *coc-config-extensions-updateUIInTab* Open `CocUpdate` UI in new tab. Scope: `application`, default: `false` "extensions.recommendations" *coc-config-extensions-recommendations* List of extensions recommended for installation in the current project. Only works as workspace folder configuration. Scope: `resource`, default: `[]` ------------------------------------------------------------------------------ PREFERENCES *coc-config-preferences* "coc.preferences.bracketEnterImprove" *coc-preferences-bracketEnterImprove* Improve enter inside bracket `<> {} [] ()` by add new empty line below and place cursor to it. Works with `coc#on_enter()` Scope: `language-overridable`, default: `true` "coc.preferences.currentFunctionSymbolAutoUpdate" *coc-preferences-currentFunctionSymbolAutoUpdate* Automatically update the value of b:coc_current_function on CursorMove event Scope: `language-overridable`, default: `false` "coc.preferences.currentFunctionSymbolDebounceTime" *coc-preferences-currentFunctionSymbolDebounceTime* Set debounce timer for the update of b:coc_current_function on CursorMove event Scope: `application`, default: `300` "coc.preferences.enableLinkedEditing" *coc-preferences-enableLinkedEditing* Enable linked editing support. Scope: `language-overridable`, default: `false` "coc.preferences.enableMarkdown" *coc-preferences-enableMarkdown* Tell the language server that markdown text format is supported, note that markdown text may not rendered as expected. Scope: `application`, default: `true` "coc.preferences.enableMessageDialog" *coc-preferences-enableMessageDialog* Enable messages shown in notification dialog. Deprecated, prefer configuration 'coc.preferences.messageDialogKind' instead. Scope: `application`, default: `false` "coc.preferences.messageDialogKind" *coc-preferences-messageDialogKind* Configure the type of user interaction when an interactive message dialog occurs with more than zero actions to trigger. Scope: `application`, default: `confirm` "coc.preferences.messageReportKind" *coc-preferences-messageReportKind* Configure the type of user interaction when a non-interactive message dialog occurs with zero actions to trigger. Scope: `application`, default: `echo` "coc.preferences.excludeImageLinksInMarkdownDocument" *coc-preferences-excludeImageLinksInMarkdownDocument* Exclude image links from markdown text in float window. Scope: `application`, default: `true` "coc.preferences.enableGFMBreaksInMarkdownDocument" *coc-preferences-enableGFMBreaksInmakrdownDocument* Exclude GFM breaks in markdown document. Scope: `application`, default: `true` "coc.preferences.floatActions" *coc-preferences-floatActions* Set to false to disable float/popup support for actions menu. Scope: `application`, default: `true` "coc.preferences.formatOnSave" *coc-preferences-formatOnSave* Set to true to enable formatting on save. Scope: `language-overridable`, default: `false` "coc.preferences.formatOnSaveTimeout" *coc-preferences-formatOnSaveTimeout* How long before the format command run on save will time out. Scope: `language-overridable`, default: `200` "coc.preferences.formatOnType" *coc-preferences-formatOnType* Set to true to enable formatting on typing Scope: `language-overridable`, default: `false` "coc.preferences.formatterExtension" *coc-preferences-formatterExtension* Extension used for formatting documents. When set to null, the formatter with highest priority is used. Scope: `language-overridable`, default: `null` "coc.preferences.jumpCommand" *coc-preferences-jumpCommand* Command used for location jump, like goto definition, goto references etc. Can be also a custom command that gives file as an argument. Scope: `application`, default: `"edit"` "coc.preferences.maxFileSize" *coc-preferences-maxFileSize* Maximum file size in bytes that coc.nvim should handle, default '10MB'. Scope: `application`, default: `"10MB"` "coc.preferences.messageLevel" *coc-preferences-messageLevel* Message level for filter echoed messages, could be 'more', 'warning' and 'error' Scope: `application`, default: `"more"` "coc.preferences.promptInput" *coc-preferences-promptInput* Use prompt buffer in float window for user input. Scope: `application`, default: `true` "coc.preferences.renameFillCurrent" *coc-preferences-renameFillCurrent* Disable to stop Refactor-Rename float/popup window from populating with old name in the New Name field. Scope: `application`, default: `true` "coc.preferences.silentAutoupdate" *coc-preferences-silentAutoupdate* Not open split window with update status when performing auto update. Scope: `application`, default: `true` "coc.preferences.useQuickfixForLocations" *coc-preferences-useQuickfixForLocations* Use vim's quickfix list for jump locations, need restart on change. Scope: `application`, default: `false` "coc.preferences.watchmanPath" *coc-preferences-watchmanPath* executable path for https://facebook.github.io/watchman/, detected from $PATH by default. Required by features which need file watch to work, eg: update import path on file move. Scope: `application`, default: `null` "coc.preferences.willSaveHandlerTimeout" *coc-preferences-willSaveHandlerTimeout* Will save handler timeout. Scope: `application`, default: `500` "coc.preferences.tagDefinitionTimeout" *coc-preferences-tagDefinitionTimeout* The timeout of CocTagFunc. Scope: `application`, default: `0` ------------------------------------------------------------------------------ EDITOR *coc-config-editor* *coc-config-editor-codeActionsOnSave* "editor.codeActionsOnSave" Run Code Actions for the buffer on save, normally source actions, Example: `\"source.organizeImports\": \"always\"`, |coc-preferences-willSaveHandlerTimeout| is used for timeout control. Scope: `language-overridable`, default: `{}` *coc-config-editor-autocmdTimeout* "editor.autocmdTimeout" Timeout for execute request autocmd registered by coc extensions. Scope: `application`, default: `1000` ------------------------------------------------------------------------------ FLOATfACTORY *coc-config-floatFactory* "floatFactory.floatConfig" *coc-config-floatFactory-floatConfig* Configure default float window/popup style created by float factory (created around cursor and automatically closed), see |coc-config-float| for supported properties. Scope: `application`, default: `null` ------------------------------------------------------------------------------ FLOAT CONFIGURATION *coc-config-float* Used by `floatFactory.floatConfig`, `suggest.floatConfig`, `diagnostic.floatConfig`, `signature.floatConfig` and `hover.floatConfig`, following properties are supported: - "border": Change to `true` to enable border. - "rounded": Use rounded borders when border is `true`. - "highlight": Background highlight group of float window, default: `"CocFloating"`. - "title": Title text used by float window, default: `""`. - "borderhighlight": Border highlight group of float window, default: `"CocFloatBorder"`. - "close": Set to `true` to draw close icon. - "maxWidth": Maximum width of float window, contains border. - "maxHeight": Maximum height of float window, contains border. - "winblend": Set 'winblend' option of window, neovim only, default: `0`. - "focusable": Set to false to make window not focusable, neovim only. - "shadow": Set to true to enable shadow, neovim only. - "position": Controls how floating windows are positioned. When set to `'fixed'`, the window will be positioned according to the `top`, `bottom`, `left`, and `right` settings. When set to `'auto'`, the window follows the default position. - "top": Distance from the top of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Will be ignored if `bottom` is set. - "bottom": Distance from the bottom of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Takes precedence over `top` if both are set. - "left": Distance from the left edge of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Will be ignored if `right` is set. - "right": "Distance from the right edge of the editor window in characters. Only takes effect when `position` is set to `'fixed'`. Takes precedence over `left` if both are set." ------------------------------------------------------------------------------ TREE *coc-config-tree* Configurations for tree view. "tree.openedIcon" *coc-config-tree-openedIcon* Opened icon of tree view. Scope: `application`, default: `"-"` "tree.closedIcon" *coc-config-tree-closedIcon* Closed icon of tree view. Scope: `application`, default: `"+"` "tree.key.actions" *coc-config-tree-key-actions* Trigger key to invoke actions. Scope: `application`, default: `""` "tree.key.activeFilter" *coc-config-tree-key-activeFilter* Trigger key active filter. Scope: `application`, default: `"f"` "tree.key.close" *coc-config-tree-key-close* Trigger key to dispose the tree and close tree window. Scope: `application`, default: `""` "tree.key.collapseAll" *coc-config-tree-key-collapseAll* Trigger key to collapse all tree node. Scope: `application`, default: `"M"` "tree.key.invoke" *coc-config-tree-key-invoke* Trigger key to invoke default command of current node or selection. Scope: `application`, default: `""` "tree.key.selectNext" *coc-config-tree-key-selectNext* Trigger key to select next item during filter. Scope: `application`, default: `""` "tree.key.selectPrevious" *coc-config-tree-key-selectPrevious* Trigger key to select previous item during filter. Scope: `application`, default: `""` "tree.key.toggle" *coc-config-tree-key-toggle* Trigger key to toggle expand state of tree node, does nothing with leaf node. Scope: `application`, default: `"t"` "tree.key.toggleSelection" *coc-config-tree-key-toggleSelection* Trigger key to select/unselect item. Scope: `application`, default: `""` ------------------------------------------------------------------------------ DIALOG *coc-config-dialog* Configurations for dialog windows. "dialog.confirmKey" *coc-config-dialog-confirmKey* Confirm key for confirm selection used by menu and picker, you can always use to cancel. Scope: `application`, default: `""` "dialog.floatBorderHighlight" *coc-config-dialog-floatBorderHighlight* Highlight group for border of dialog window/popup, use 'CocFloating' when not specified. Scope: `application`, default: `null` "dialog.floatHighlight" *coc-config-dialog-floatHighlight* Highlight group for dialog window/popup, use 'CocFloating' when not specified. Scope: `application`, default: `null` "dialog.maxHeight" *coc-config-dialog-maxHeight* Maximum height of dialog window, for quickpick, it's content window's height. Scope: `application`, default: `30` "dialog.maxWidth" *coc-config-dialog-maxWidth* Maximum width of dialog window. Scope: `application`, default: `80` "dialog.pickerButtonShortcut" *coc-config-dialog-pickerButtonShortcut* Show shortcut in buttons of picker dialog window/popup, used when dialog .pickerButtons is true. Scope: `application`, default: `true` "dialog.pickerButtons" *coc-config-dialog-pickerButtons* Show buttons for picker dialog window/popup. Scope: `application`, default: `true` "dialog.rounded" *coc-config-dialog-rounded* use rounded border for dialog window. Scope: `application`, default: `true` "dialog.shortcutHighlight" *coc-config-dialog-shortcutHighlight* Highlight group for shortcut character in menu dialog. Scope: `application`, default: `"MoreMsg"` ------------------------------------------------------------------------------ HTTP PROXY *coc-config-http* Configurations for http requests, used by coc.nvim and some coc extensions. "http.proxy" *coc-config-http-proxy* The proxy setting to use. If not set, will be inherited from the ` http_proxy` and `https_proxy` environment variables. Scope: `application`, default: `""` "http.proxyAuthorization" *coc-config-http-proxyAuthorization* The value to send as the `Proxy-Authorization` header for every network request. Scope: `application`, default: `null` "http.proxyCA" *coc-config-http-proxyCA* CA (file) to use as Certificate Authority> Scope: `application`, default: `null` "http.proxyStrictSSL" *coc-config-http-proxyStrictSSL* Controls whether the proxy server certificate should be verified against the list of supplied CAs. Scope: `application`, default: `true` ------------------------------------------------------------------------------ NPM *coc-config-npm* "npm.binPath" *coc-config-npm-binPath* Command or absolute path to npm or yarn for global extension install/uninstall. Scope: `application`, default: `"npm"` ------------------------------------------------------------------------------ LANGUAGESERVER *coc-config-languageserver* Dictionary of Language Servers, key is the ID of corresponding server, and value is configuration of languageserver. Default: `{}` Properties of languageserver configuration: - "enable": Change to `false` to disable that languageserver. - "filetypes": Supported filetypes, add * in array for all filetypes. Note: it's required for start the languageserver, please make sure your filetype is expected by `:CocCommand document.echoFiletype` command - 'maxRestartCount': Maximum restart count when server closed in the last 3 minutes, default to `4`. - "additionalSchemes": Additional URI schemes, default schemes including file & untitled. Note: you have to setup vim provide content for custom URI as well. - "cwd": Working directory used to start languageserver, vim's cwd is used by default. - "env": Environment variables for child process. - "settings": Settings for languageserver, received on server initialization. - "trace.server": Trace level of communication between server and client that showed with output channel, open output channel by command `:CocCommand workspace.showOutput` - "stdioEncoding": Encoding used for stdio of child process. - "initializationOptions": Initialization options passed to languageserver (it's deprecated) - "rootPatterns": Root patterns used to resolve rootPath from current file. - "requireRootPattern": If true, doesn't start server when root pattern not found. - "ignoredRootPaths": Absolute root paths that language server should not use as rootPath, higher priority than rootPatterns. - "disableDynamicRegister": Disable dynamic registerCapability feature for this languageserver to avoid duplicated feature registration. - "disableSnippetCompletion": Disable snippet completion feature for this languageserver. - "disabledFeatures": Disable features for this languageserver, valid keys: > ["completion", "configuration", "workspaceFolders", "diagnostics", "willSave", "willSaveUntil", "didSaveTextDocument", "fileSystemWatcher", "hover", "signatureHelp", "definition", "references", "documentHighlight", "documentSymbol", "workspaceSymbol", "codeAction", "codeLens", "formatting", "documentFormatting", "documentRangeFormatting", "documentOnTypeFormatting", "rename", "documentLink", "executeCommand", "pullConfiguration", "typeDefinition", "implementation", "declaration", "color", "foldingRange", "selectionRange", "progress", "callHierarchy", "linkedEditing", "fileEvents", "semanticTokens"] < - "formatterPriority": Priority of this languageserver's formatter. - "revealOutputChannelOn": Configure message level to show the output channel buffer. - "progressOnInitialization": Enable progress report on languageserver initialize. Language server start with command: ~ Additional fields can be used for a command languageserver: - "command": Executable program name in $PATH or absolute path of executable used for start languageserver. - "args": Command line arguments of command. - "detached": Detach language server when is true. - "shell": Use shell for server process, default: `false` Language server start with module: ~ Additional fields can be used for a languageserver started by node module: - "module": Absolute filepath of Javascript file. - "args": Extra arguments used on fork Javascript module. - "runtime": Absolute path of node runtime, node runtime of coc.nvim is used by default. - "execArgv": ARGV passed to node on fork, normally used for debugging, example: `["--nolazy", "--inspect-brk=6045"]` - "transport": Transport kind used by server, could be 'ipc', 'stdio', 'socket' and 'pipe'. 'ipc' is used by default (recommended). - "transportPort": Port number used when transport is 'socket'. Language server use initialized socket server: ~ - "port": Port number of socket server. - "host": Host of socket server, default to `127.0.0.1`. ============================================================================== LSP FEATURES Configurations for features provided by language server. ------------------------------------------------------------------------------ CALLHIERARCHY *coc-config-callHierarchy* "callHierarchy.enableTooltip" *coc-config-callHierarchy-enableTooltip* Enable tooltip to show relative filepath of call hierarchy item. Scope: `application`, default: `true` "callHierarchy.openCommand" *coc-config-callHierarchy-openCommand* Open command for call hierarchy tree view. Scope: `application`, default: `"edit"` "callHierarchy.splitCommand" *coc-config-callHierarchy-splitCommand* Window split command used by call hierarchy tree view. Scope: `application`, default: `"botright 30vs"` ------------------------------------------------------------------------------ CODELENS *coc-config-codeLens* "codeLens.enable" *coc-config-codeLens-enable* Enable codeLens feature, require neovim with set virtual text feature. Scope: `language-overridable`, default: `false` "codeLens.display" *coc-config-codeLens-display* Display codeLens. Toggle with :CocCommand document.toggleCodeLens Scope: `language-overridable`, default: `true` "codeLens.position" *coc-config-codeLens-position* Display position of codeLens virtual text. Scope: `resource`, default: `"top"` "codeLens.separator" *coc-config-codeLens-separator* Separator text for codeLens in virtual text. Scope: `resource`, default: `""` "codeLens.subseparator" *coc-config-codeLens-subseparator* Subseparator between codeLenses in virtual text. Scope: `resource`, default: `" | "` ------------------------------------------------------------------------------ COLORS *coc-config-colors* "colors.enable" *coc-config-colors-enable* Enable colors highlight feature, for terminal vim, 'termguicolors' option should be enabled and the terminal support gui colors. Scope: `language-overridable`, default: `false` "colors.highlightPriority" *coc-config-colors-highlightPriority* Priority for colors highlights, works on vim8 and neovim >= 0.6.0. Scope: `application`, default: `1000` ------------------------------------------------------------------------------ CURSORS *coc-config-cursors* "cursors.cancelKey" *coc-config-cursors-cancelKey* Key used for cancel cursors session. Scope: `application`, default: `""` "cursors.nextKey" *coc-config-cursors-nextKey* Key used for jump to next cursors position. Scope: `application`, default: `""` "cursors.previousKey" *coc-config-cursors-previousKey* Key used for jump to previous cursors position. Scope: `application`, default: `""` "cursors.wrapscan" *coc-config-cursors-wrapscan* Searches wrap around the first or last cursors range. Scope: `application`, default: `true` ------------------------------------------------------------------------------ DIAGNOSTIC *coc-config-diagnostic* "diagnostic.autoRefresh" *coc-config-diagnostic-autoRefresh* Enable automatically refresh diagnostics, use diagnosticRefresh action when it's disabled. Scope: `language-overridable`, default: `true` "diagnostic.checkCurrentLine" *coc-config-diagnostic-checkCurrentLine* When enabled, show all diagnostics of current line if there are none at the current position. Scope: `language-overridable`, default: `false` "diagnostic.displayByAle" *coc-config-diagnostic-displayByAle* Use Ale, coc-diagnostics-shim.nvim, or other provider to display diagnostics in vim. This setting will disable diagnostic display using coc's handler. A restart required on change. Scope: `language-overridable`, default: `false` "diagnostic.displayByVimDiagnostic" *coc-config-diagnostic-displayByVimDiagnostic* Set diagnostics to nvim's `vim.diagnostic`, and prevent coc.nvim's handler to display in virtualText/floating window etc. Scope: `language-overridable`, default: `false` "diagnostic.enable" *coc-config-diagnostic-enable* Set to false to disable diagnostic display. Scope: `language-overridable`, default: `true` "diagnostic.enableHighlightLineNumber" *coc-config-diagnostic-enableHighlightLineNumber* Enable highlighting line numbers for diagnostics, only works with neovim. Scope: `application`, default: `true` "diagnostic.enableMessage" *coc-config-diagnostic-enableMessage* When to enable show messages of diagnostics. Scope: `application`, default: `"always"` "diagnostic.enableSign" *coc-config-diagnostic-enableSign* Enable signs for diagnostics. Scope: `language-overridable`, default: `true` "diagnostic.errorSign" *coc-config-diagnostic-errorSign* Text of error sign. Scope: `application`, default: `">>"` "diagnostic.filetypeMap" *coc-config-diagnostic-filetypeMap* A map between buffer filetype and the filetype assigned to diagnostics. To syntax highlight diagnostics with their parent buffer type use `" default": "bufferType"`. Scope: `application`, default: `{}` "diagnostic.floatConfig" *coc-config-diagnostic-floatConfig* Configuration of floating window/popup for diagnostic messages, see |coc-config-float|. Scope: `application`, default: `null` "diagnostic.format" *coc-config-diagnostic-format* Define the diagnostic format that shown in float window or echoed, available parts: source, code, severity, message. Scope: `language-overridable`, default: `"%message (%source%code)"` "diagnostic.highlightLimit" *coc-config-diagnostic-highlightLimit* Limit count for highlighted diagnostics, too many diagnostic highlights could make vim stop responding. Scope: `language-overridable`, default: `1000` "diagnostic.highlightPriority" *coc-config-diagnostic-highlightPriority* Priority for diagnostic highlights, works on vim8 and neovim >= 0.6.0. Scope: `language-overridable`, default: `4096` "diagnostic.hintSign" *coc-config-diagnostic-hintSign* Text of hint sign. Scope: `application`, default: `">>"` "diagnostic.infoSign" *coc-config-diagnostic-infoSign* Text of info sign. Scope: `application`, default: `">>"` "diagnostic.level" *coc-config-diagnostic-level* Used for filter diagnostics by diagnostic severity. Scope: `resource`, default: `"hint"` "diagnostic.locationlistLevel" *coc-config-diagnostic-locationlistLevel* Filter diagnostics in locationlist. Scope: `language-overridable`, default: `null` "diagnostic.locationlistUpdate" *coc-config-diagnostic-locationlistUpdate* Update locationlist on diagnostics change, only works with locationlist opened by :CocDiagnostics command and first window of associated buffer. Scope: `language-overridable`, default: `true` "diagnostic.messageDelay" *coc-config-diagnostic-messageDelay* How long to wait (in milliseconds) before displaying the diagnostic message with echo or float Scope: `application`, default: `200` "diagnostic.messageLevel" *coc-config-diagnostic-messageLevel* Filter diagnostic message in float window/popup. Scope: `language-overridable`, default: `null` "diagnostic.messageTarget" *coc-config-diagnostic-messageTarget* Diagnostic message target. Scope: `language-overridable`, default: `"float"` "diagnostic.refreshOnInsertMode" *coc-config-diagnostic-refreshOnInsertMode* Enable diagnostic refresh on insert mode, default false. Scope: `language-overridable`, default: `false` "diagnostic.showDeprecated" *coc-config-diagnostic-showDeprecated* Show diagnostics with deprecated tag. Scope: `language-overridable`, default: `true` "diagnostic.showUnused" *coc-config-diagnostic-showUnused* Show diagnostics with unused tag, affects highlight, sign, virtual text , message. Scope: `language-overridable`, default: `true` "diagnostic.signLevel" *coc-config-diagnostic-signLevel* Filter diagnostics displayed in signcolumn. Scope: `language-overridable`, default: `null` "diagnostic.signPriority" *coc-config-diagnostic-signPriority* Priority of diagnostic signs. Scope: `resource`, default: `10` "diagnostic.virtualText" *coc-config-diagnostic-virtualText* Use virtual text to display diagnostics. Scope: `language-overridable`, default: `false` "diagnostic.virtualTextAlign" *coc-config-diagnostic-virtualTextAlign* Position of virtual text. Scope: `language-overridable`, default: `"after"` "diagnostic.virtualTextCurrentLineOnly" *coc-config-diagnostic-virtualTextCurrentLineOnly* Only show virtualText diagnostic on current cursor line. Scope: `language-overridable`, default: `true` "diagnostic.virtualTextFormat" *coc-config-diagnostic-virtualTextFormat* Define the virtual text diagnostic format, available parts: source, code , severity, message. Scope: `language-overridable`, default: `"%message"` "diagnostic.virtualTextLevel" *coc-config-diagnostic-virtualTextLevel* Filter diagnostic message in virtual text by level. Scope: `language-overridable`, default: `null` "diagnostic.virtualTextLimitInOneLine" *coc-config-diagnostic-virtualTextLimitInOneLine* The maximum number of diagnostic messages to display in one line. Scope: `language-overridable`, default: `999` "diagnostic.virtualTextLineSeparator" *coc-config-diagnostic-virtualTextLineSeparator* The text that will mark a line end from the diagnostic message. Scope: `language-overridable`, default: `" \ "` "diagnostic.virtualTextLines" *coc-config-diagnostic-virtualTextLines* The number of non empty lines from a diagnostic to display. Scope: `language-overridable`, default: `3` "diagnostic.virtualTextPrefix" *coc-config-diagnostic-virtualTextPrefix* The prefix added virtual text diagnostics. Scope: `language-overridable`, default: `" "` "diagnostic.virtualTextWinCol" *coc-config-diagnostic-virtualTextWinCol* Window column number to align virtual text, neovim only. Scope: `language-overridable`, default: `null` "diagnostic.warningSign" *coc-config-diagnostic-warningSign* Text of warning sign. Scope: `application`, default: `"⚠"` "diagnostic.showRelatedInformation" *coc-config-diagnostic-showRelatedInformation* Display related information in the diagnostic floating window. Scope: `language-overridable`, default: `true` ------------------------------------------------------------------------------ DOCUMENTHIGHLIGHT *coc-config-documentHighlight* "documentHighlight.priority" *coc-config-documentHighlight-priority* Match priority used by document highlight, see ':h matchadd'. Scope: `resource`, default: `-1` "documentHighlight.limit" *coc-config-documentHighlight-limit* Limit the highlights added by matchaddpos, too many positions could cause vim slow. Scope: `resource`, default: `100` "documentHighlight.timeout" *coc-config-documentHighlight-timeout* Timeout for document highlight, in milliseconds. Scope: `resource`, default: `300` ------------------------------------------------------------------------------ HOVER *coc-config-hover* "hover.autoHide" *coc-config-hover-autoHide* Automatically hide hover float window on CursorMove or InsertEnter. Scope: `application`, default: `true` "hover.floatConfig" *coc-config-hover-floatConfig* Configuration of floating window/popup for hover documents, see |coc-config-float|. Scope: `application`, default: `null` "hover.previewMaxHeight" *coc-config-hover-previewMaxHeight* Max height of preview window for hover. Scope: `resource`, default: `12` "hover.target" *coc-config-hover-target* Target to show hover information, could be `float`, `echo` or `preview`. Scope: `resource`, default: `float` ------------------------------------------------------------------------------ INLAYHINT *coc-config-inlayHint* "inlayHint.enable" *coc-config-inlayHint-enable* Enable inlay hint support. Scope: `language-overridable`, default: `true` "inlayHint.enableParameter" *coc-config-inlayHint-enableParameter* Enable inlay hints for parameters. Scope: `language-overridable`, default: `true` "inlayHint.display" *coc-config-inlayHint-display* Display inlay hints. Toggle with :CocCommand document.toggleInlayHint Scope: `language-overridable`, default: `true` "inlayHint.refreshOnInsertMode" *coc-config-inlayHint-refreshOnInsertMode* Refresh inlayHints on insert mode. Scope: `language-overridable`, default: `false` "inlayHint.position" *coc-config-inlayHint-position* Controls the position of inlay hint, supports `inline` and `eol`. Scope: `language-overridable`, default: `inline` ------------------------------------------------------------------------------ LINK *coc-config-links* "links.enable" *coc-config-links-enable* Enable document links. Scope: `language-overridable`, default: `true` "links.highlight" *coc-config-links-highlight* Use CocLink highlight group to highlight links. Scope: `application`, default: `false` "links.tooltip" *coc-config-links-tooltip* Show tooltip of link under cursor on CursorHold. Scope: `application`, default: `false` ------------------------------------------------------------------------------ LIST *coc-config-list* "list.alignColumns" *coc-config-list-alignColumns* Whether to align lists in columns. Scope: `application`, default: `false` "list.extendedSearchMode" *coc-config-list-extendedSearchMode* Enable extended search mode which allows multiple search patterns delimited by spaces. Scope: `application`, default: `true` "list.floatPreview" *coc-config-list-floatPreview* Enable preview with float window/popup, default: `false`. Scope: `application`, default: `false` "list.height" *coc-config-list-height* Height of split list window. Scope: `application`, default: `10` "list.indicator" *coc-config-list-indicator* The character used as first character in prompt line. Scope: `application`, default: `">"` "list.insertMappings" *coc-config-list-insertMappings* Custom keymappings on insert mode. Scope: `application`, default: `{}` "list.interactiveDebounceTime" *coc-config-list-interactiveDebounceTime* Debounce time for input change on interactive mode. Scope: `application`, default: `100` "list.limitLines" *coc-config-list-limitLines* Limit lines for list buffer. Scope: `application`, default: `null` "list.maxPreviewHeight" *coc-config-list-maxPreviewHeight* Max height for preview window of list. Scope: `application`, default: `12` "list.menuAction" *coc-config-list-menuAction* Use menu picker instead of confirm() for choose action. Scope: `application`, default: `false` "list.nextKeymap" *coc-config-list-nextKeymap* Key used for select next line on insert mode. Scope: `application`, default: `""` "list.normalMappings" *coc-config-list-normalMappings* Custom keymappings on normal mode. Scope: `application`, default: `{}` "list.previewHighlightGroup" *coc-config-list-previewHighlightGroup* Highlight group used for highlight the range in preview window. Scope: `application`, default: `"Search"` "list.previewSplitRight" *coc-config-list-previewSplitRight* Use vsplit for preview window. Scope: `application`, default: `false` "list.previewToplineOffset" *coc-config-list-previewToplineOffset* Topline offset for list previews Scope: `application`, default: `3` "list.previewToplineStyle" *coc-config-list-previewToplineStyle* Topline style for list previews, could be "offset" or "middle". Scope: `application`, default: `"offset"` "list.previousKeymap" *coc-config-list-previousKeymap* Key used for select previous line on insert mode. Scope: `application`, default: `""` "list.selectedSignText" *coc-config-list-selectedSignText* Sign text for selected lines. Scope: `application`, default: `"*"` "list.signOffset" *coc-config-list-signOffset* Sign offset of list, should be different from other plugins. Scope: `application`, default: `900` "list.smartCase" *coc-config-list-smartCase* Use smartcase match for fuzzy match and strict match, --ignore-case will be ignored, may not affect interactive list. Scope: `application`, default: `false` "list.source.diagnostics.includeCode" *coc-config-list-source-diagnostics-includeCode* Whether to show the diagnostic code in the list. Scope: `application`, default: `true` "list.source.diagnostics.pathFormat" *coc-config-list-source-diagnostics-pathFormat* Decide how the filepath is shown in the list. Scope: `application`, default: `"full"` "list.source.outline.ctagsFiletypes" *coc-config-list-source-outline-ctagsFiletypes* Filetypes that should use ctags for outline instead of language server. Scope: `application`, default: `[]` "list.source.symbols.excludes" *coc-config-list-source-symbols-excludes* Patterns of minimatch for filepath to exclude from symbols list. Scope: `application`, default: `[]` "list.statusLineSegments" *coc-config-list-statusLineSegments* An array of statusline segments that will be used to draw the status line for list windows. Scope: `application`. ------------------------------------------------------------------------------ NOTIFICATION *coc-config-notification* "notification.border" *coc-config-notification-border* Enable rounded border for notification windows. Scope: `application`, default: `true` "notification.disabledProgressSources" *coc-config-notification-disabledProgressSources* Sources that should be disabled for message progress, use * to disable all progresses. Scope: `application`, default: `[]` "notification.focusable" *coc-config-notification-focusable* Enable focus by user actions (wincmds, mouse events), neovim only. Scope: `application`, default: `true` "notification.highlightGroup" *coc-config-notification-highlightGroup* Highlight group of notification dialog. Scope: `application`, default: `"Normal"` "notification.marginRight" *coc-config-notification-marginRight* Margin right to the right of editor window. Scope: `application`, default: `10` "notification.maxHeight" *coc-config-notification-maxHeight* Maximum content height of notification dialog. Scope: `application`, default: `10` "notification.maxWidth" *coc-config-notification-maxWidth* Maximum content width of notification dialog. Scope: `application`, default: `60` "notification.minProgressWidth" *coc-config-notification-minProgressWidth* Minimal with of progress notification. Scope: `application`, default: `30` "notification.statusLineProgress" *coc-config-notification-statusLineProgress* Show progress notification in status line, instead of use float window/popup. "notification.timeout" *coc-config-notification-timeout* Timeout for auto close notifications, in milliseconds. Scope: `application`, default: `10000` "notification.winblend" *coc-config-notification-winblend* Winblend option of notification window, neovim only. Scope: `application`, default: `30` ------------------------------------------------------------------------------ OUTLINE *coc-config-outline* "outline.autoPreview" *coc-config-outline-autoPreview* Enable auto preview on cursor move. Scope: `application`, default: `false` "outline.autoHide" *coc-config-outline-autoHide* Automatically hide the outline window when an item is clicked. Scope: `application`, default: `false` "outline.autoWidth" *coc-config-outline-autoWidth* Automatically increase window width to avoid wrapped lines. Scope: `application`, default: `true` "outline.checkBufferSwitch" *coc-config-outline-checkBufferSwitch* Recreate outline view after user changed to another buffer on current tab. Scope: `application`, default: `true` "outline.codeActionKinds" *coc-config-outline-codeActionKinds* Filter code actions in actions menu by kinds. Scope: `application`, default: `["","quickfix","refactor"]` "outline.detailAsDescription" *coc-config-outline-detailAsDescription* Show detail as description aside with label, when false detail will be shown in tooltip on cursor hold. Scope: `application`, default: `true` "outline.expandLevel" *coc-config-outline-expandLevel* Expand level of tree nodes. Scope: `application`, default: `1` "outline.followCursor" *coc-config-outline-followCursor* Reveal item in outline tree on cursor hold. Scope: `application`, default: `true` "outline.keepWindow" *coc-config-outline-keepWindow* Jump back to original window after outline is shown. Scope: `application`, default: `false` "outline.previewBorder" *coc-config-outline-previewBorder* Use border for preview window. Scope: `application`, default: `true` "outline.previewBorderHighlightGroup" *coc-config-outline-previewBorderHighlightGroup* Border highlight group of preview window. Scope: `application`, default: `"Normal"` "outline.previewBorderRounded" *coc-config-outline-previewBorderRounded* Use rounded border for preview window. Scope: `application`, default: `false` "outline.previewHighlightGroup" *coc-config-outline-previewHighlightGroup* Highlight group of preview window. Scope: `application`, default: `"Normal"` "outline.previewMaxWidth" *coc-config-outline-previewMaxWidth* Max width of preview window. Scope: `application`, default: `80` "outline.previewWinblend" *coc-config-outline-previewWinblend* Enables pseudo-transparency by set 'winblend' option of window, neovim only. Scope: `application`, default: `0` "outline.showLineNumber" *coc-config-outline-showLineNumber* Show line number of symbols. Scope: `application`, default: `true` "outline.sortBy" *coc-config-outline-sortBy* Default sort method for symbols outline. Scope: `application`, default: `"category"` "outline.splitCommand" *coc-config-outline-splitCommand* Window split command used by outline. Scope: `application`, default: `"botright 30vs"` "outline.switchSortKey" *coc-config-outline-switchSortKey* The key used to switch sort method for symbols provider of current tree view. Scope: `application`, default: `""` "outline.togglePreviewKey" *coc-config-outline-togglePreviewKey* The key used to toggle auto preview feature. Scope: `application`, default: `"p"` ------------------------------------------------------------------------------ PULLDIAGNOSTIC *coc-config-pullDiagnostic* "pullDiagnostic.ignored" *coc-config-pullDiagnostic-ignored* Minimatch patterns to match full filepath that should be ignored for pullDiagnostic. Scope: `application`, default: `[]` "pullDiagnostic.onChange" *coc-config-pullDiagnostic-onChange* Whether to pull for diagnostics on document change. Scope: `language-overridable`, default: `true` "pullDiagnostic.onSave" *coc-config-pullDiagnostic-onSave* Whether to pull for diagnostics on document save. Scope: `language-overridable`, default: `false` "pullDiagnostic.workspace" *coc-config-pullDiagnostic-workspace* Whether to pull for workspace diagnostics when possible. Scope: `application`, default: `true` ------------------------------------------------------------------------------ REFACTOR *coc-config-refactor* "refactor.afterContext" *coc-config-refactor-afterContext* Print num lines of trailing context after each match. Scope: `application`, default: `3` "refactor.beforeContext" *coc-config-refactor-beforeContext* Print num lines of leading context before each match. Scope: `application`, default: `3` "refactor.openCommand" *coc-config-refactor-openCommand* Open command for refactor window. Scope: `application`, default: `"vsplit"` "refactor.saveToFile" *coc-config-refactor-saveToFile* Save changed buffer to file when write refactor buffer with ':noa wa' command. Scope: `application`, default: `true` "refactor.showMenu" *coc-config-refactor-showMenu* Refactor buffer local mapping to bring up menu for this chunk. Scope: `application`, default: `""` ------------------------------------------------------------------------------ SEMANTICTOKENS *coc-config-semanticTokens* "semanticTokens.combinedModifiers" *coc-config-semanticTokens-combinedModifiers* Semantic token modifiers that should have highlight combined with syntax highlights. Scope: `language-overridable`, default: `["deprecated"]` "semanticTokens.enable" *coc-config-semanticTokens-enable* Enable semantic tokens support. Scope: `language-overridable`, default: `false` "semanticTokens.highlightPriority" *coc-config-semanticTokens-highlightPriority* Priority for semantic tokens highlight. Scope: `language-overridable`, default: `2048` "semanticTokens.incrementTypes" *coc-config-semanticTokens-incrementTypes* Semantic token types that should increase highlight when insert at the start and end position of token. Scope: `language-overridable`, default: `["variable","string","parameter"]` ------------------------------------------------------------------------------ SIGNATURE *coc-config-signature* "signature.enable" *coc-config-signature-enable* Enable show signature help when trigger character typed. Scope: `language-overridable`, default: `true` "signature.floatConfig" *coc-config-signature-floatConfig* Configuration of floating window/popup for signature documents, see |coc-config-float|. Scope: `application`, default: `null` "signature.hideOnTextChange" *coc-config-signature-hideOnTextChange* Hide signature float window when text changed on insert mode. Scope: `language-overridable`, default: `false` "signature.preferShownAbove" *coc-config-signature-preferShownAbove* Show signature help float window above cursor when possible, require restart coc.nvim on change. Scope: `application`, default: `true` "signature.target" *coc-config-signature-target* Target of signature help, use float when possible by default. Scope: `language-overridable`, default: `"float"` "signature.triggerSignatureWait" *coc-config-signature-triggerSignatureWait* Timeout for trigger signature help, in milliseconds. Scope: `language-overridable`, default: `500` ------------------------------------------------------------------------------ SNIPPET *coc-config-snippet* "snippet.highlight" *coc-config-snippet-highlight* Use highlight group 'CocSnippetVisual' to highlight placeholders with same index of current one. Scope: `resource`, default: `false` "snippet.nextPlaceholderOnDelete" *coc-config-snippet-nextPlaceholderOnDelete* Automatically jump to the next placeholder when the current one is completely deleted. Scope: `resource`, default: `false` "snippet.statusText" *coc-config-snippet-statusText* Text shown in statusline to indicate snippet session is activated. Scope: `application`, default: `"SNIP"` ------------------------------------------------------------------------------ SUGGEST *coc-config-suggest* "suggest.acceptSuggestionOnCommitCharacter" *coc-config-suggest-acceptSuggestionOnCommitCharacter* Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character. Scope: `language-overridable`, default: `false` "suggest.asciiCharactersOnly" *coc-config-suggest-asciiCharactersOnly* Trigger suggest with ASCII characters only. Scope: `language-overridable`, default: `false` "suggest.segmenterLocales" *coc-config-suggest-segmenterLocales* Locales used for divide sentence into segments for around and buffer source, works when NodeJS built with intl support, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter#parameters default empty string means auto detect, use null to disable this feature. Scope: `language-overridable`, default: `""` "suggest.asciiMatch" *coc-config-suggest-asciiMatch* Convert unicode characters to ascii for match. Scope: `language-overridable`, default: `true` "suggest.autoTrigger" *coc-config-suggest-autoTrigger* How should completion be triggered, could be `"always"`, `"trigger"` or `"none"`. Scope: `language-overridable`, default: `"always"` "suggest.completionItemKindLabels" *coc-config-suggest-completionItemKindLabels* Set custom labels to completion items' kinds. Default value: > { "text": "v", "method": "f", "function": "f", "constructor": "f", "field": "m", "variable": "v", "class": "C", "interface": "I", "module": "M", "property": "m", "unit": "U", "value": "v", "enum": "E", "keyword": "k", "snippet": "S", "color": "v", "file": "F", "reference": "r", "folder": "F", "enumMember": "m", "constant": "v", "struct": "S", "event": "E", "operator": "O", "typeParameter": "T", "default": "" } < Scope: `application` "suggest.defaultSortMethod" *coc-config-suggest-defaultSortMethod* Default sorting behavior when trigger is empty, could be `"length"`, `"alphabetical"` or `"none"`. Scope: `language-overridable`, default: `"length"` "suggest.detailField" *coc-config-suggest-detailField* Where to show the detail text of CompleteItem from language server. Scope: `application`, default: `"preview"` "suggest.enableFloat" *coc-config-suggest-enableFloat* Enable float window with documentation aside with popupmenu. Scope: `language-overridable`, default: `true` "suggest.enablePreselect" *coc-config-suggest-enablePreselect* Enable preselect feature, works when |coc-config-suggest-noselect| is false. Scope: `application`, default: `true` "suggest.filterGraceful" *coc-config-suggest-filterGraceful* Controls whether filtering and sorting suggestions accounts for small typos. Scope: `language-overridable`, default: `true` "suggest.filterOnBackspace" *coc-config-suggest-filterOnBackspace* Filter complete items on backspace. Scope: `language-overridable`, default: `true` "suggest.floatConfig" *coc-config-suggest-floatConfig* Configure style of popup menu and documentation window for completion, see |coc-config-float|. Note: some properties not work, including: "focusable", "close" and "maxHeight" (use 'pumheight' option for maximum height of popup menu). Note: "maxWidth" not works for popup menu, use |coc-config-suggest-labelMaxLength| instead. "suggest.formatItems" *coc-config-suggest-formatItems* Items shown in popup menu in order. Scope: `application`, default: `["abbr","menu","kind","shortcut"]` "suggest.highPrioritySourceLimit" *coc-config-suggest-highPrioritySourceLimit* Max items count for source priority bigger than or equal to 90. Scope: `language-overridable`, default: `null` "suggest.insertMode" *coc-config-suggest-insertMode* Controls whether words are overwritten when accepting completions. Scope: `language-overridable`, default: `“replace"` "suggest.ignoreRegexps" *coc-config-suggest-ignoreRegexps* Regexps to ignore when trigger suggest. Scope: `language-overridable`, default: `[]` "suggest.invalidInsertCharacters" *coc-config-suggest-invalidInsertCharacters* Invalid character for strip valid word when inserting text of complete item. Scope: `application`, default: `["\r","\n"]` "suggest.labelMaxLength" *coc-config-suggest-labelMaxLength* Max length of abbr that shown as label of complete item. Scope: `application`, default: `200` "suggest.languageSourcePriority" *coc-config-suggest-languageSourcePriority* Priority of language sources. Scope: `language-overridable`, default: `99` "suggest.localityBonus" *coc-config-suggest-localityBonus* Boost suggestions that appear closer to the cursor position. Scope: `language-overridable`, default: `true` "suggest.lowPrioritySourceLimit" *coc-config-suggest-lowPrioritySourceLimit* Max items count for source priority lower than 90. Scope: `language-overridable`, default: `null` "suggest.maxCompleteItemCount" *coc-config-suggest-maxCompleteItemCount* Maximum number of complete items shown in vim. Scope: `language-overridable`, default: `256` "suggest.minTriggerInputLength" *coc-config-suggest-minTriggerInputLength* Minimal input length for trigger completion. Scope: `language-overridable`, default: `1` "suggest.noselect" *coc-config-suggest-noselect* Not make vim select first item on popupmenu shown. Scope: `application`, default: `false` "suggest.preferCompleteThanJumpPlaceholder" *coc-config-suggest-preferCompleteThanJumpPlaceholder* Confirm completion instead of jump to next placeholder when completion is activated. Scope: `resource`, default: `false` "suggest.pumFloatConfig" *coc-config-suggest-pumFloatConfig* Configure style of popup menu, |coc-config-suggest-floatConfig| is used when not specified, see |coc-config-float|. Available properties: "border", "rounded", "highlight", "borderhighlight", "winblend", "title" and "shadow". Note: 'winblend' option is used for custom popup menu when not configured (neovim only), use 'pumwidth' for minimal width of popup menu and 'pumheight' for maximum height of popup menu. Scope: `application`, default: `null` "suggest.removeDuplicateItems" *coc-config-suggest-removeDuplicateItems* Remove completion items with duplicated word for all sources, snippet items are excluded. Scope: `language-overridable`, default: `false` "suggest.removeCurrentWord" *coc-config-suggest-removeCurrentWord* Remove word item (from around and buffer source) that is identical to current input Scope: `language-overridable`, default: `false` "suggest.reTriggerAfterIndent" *coc-config-suggest-reTriggerAfterIndent* Controls re-trigger or not after indent changes. Scope: `application`, default: `true` "suggest.reversePumAboveCursor" *coc-config-suggest-reversePumAboveCursor* Reverse order of complete items when pum shown above cursor. Scope: `application`, default: `false` "suggest.selection" *coc-config-suggest-selection* Controls how suggestions are pre-selected when showing the suggest list. Scope: `application`, default: `"first"` "suggest.snippetIndicator" *coc-config-suggest-snippetIndicator* The character used in abbr of complete item to indicate the item could be expand as snippet. Scope: `application`, default: `"~"` "suggest.snippetsSupport" *coc-config-suggest-snippetsSupport* Set to false to disable snippets support of completion. Scope: `language-overridable`, default: `true` "suggest.timeout" *coc-config-suggest-timeout* Timeout for completion, in milliseconds. Scope: `language-overridable`, default: `5000` "suggest.triggerAfterInsertEnter" *coc-config-suggest-triggerAfterInsertEnter* Trigger completion after InsertEnter, |coc-config-suggest-autoTrigger| should be 'always' to enable this option Scope: `language-overridable`, default: `false` "suggest.triggerCompletionWait" *coc-config-suggest-triggerCompletionWait* Wait time between text change and completion start, cancel completion when text changed during wait. Scope: `language-overridable`, default: `0` "suggest.virtualText" *coc-config-suggest-virtualText* Show virtual text after cursor for insert word of current selected complete item. Scope: `application`, default: `false` ------------------------------------------------------------------------------ INLINE COMPLETION *coc-config-inlineSuggest* "inlineSuggest.autoTrigger" *coc-config-inlineSuggest-autoTrigger* Enable automatically trigger inline completion after completion done or cursor hold. Scope: `language-overridable`, default: `true` "inlineSuggest.triggerCompletionWait" *coc-config-inlineSuggest-triggerCompletionWait* Wait time in milliseconds between text synchronize and trigger inline completion. Scope: `language-overridable`, default: `10` ------------------------------------------------------------------------------ TYPEHIERARCHY *coc-config-typeHierarchy* "typeHierarchy.enableTooltip" *coc-config-typeHierarchy-enableTooltip* Enable tooltip to show relative filepath of type hierarchy item. Scope: `application`, default: `true` "typeHierarchy.openCommand" *coc-config-typeHierarchy-openCommand* Open command for type hierarchy tree view. Scope: `application`, default: `"edit"` "typeHierarchy.splitCommand" *coc-config-typeHierarchy-splitCommand* Window split command used by type hierarchy tree view. Scope: `application`, default: `"botright 30vs"` ============================================================================== vim:tw=78:nosta:noet:ts=8:sts=0:ft=help:noet:fen: ================================================ FILE: doc/coc-example-config.lua ================================================ -- https://raw.githubusercontent.com/neoclide/coc.nvim/refs/heads/master/doc/coc-example-config.lua -- Some servers have issues with backup files, see #649 vim.opt.backup = false vim.opt.writebackup = false -- Having longer updatetime (default is 4000 ms = 4s) leads to noticeable -- delays and poor user experience vim.opt.updatetime = 300 -- Always show the signcolumn, otherwise it would shift the text each time -- diagnostics appeared/became resolved vim.opt.signcolumn = 'yes' local keyset = vim.keymap.set -- Autocomplete function _G.check_back_space() local col = vim.fn.col('.') - 1 return col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') ~= nil end -- Use Tab for trigger completion with characters ahead and navigate -- NOTE: There's always a completion item selected by default, you may want to enable -- no select by setting `"suggest.noselect": true` in your configuration file -- NOTE: Use command ':verbose imap ' to make sure Tab is not mapped by -- other plugins before putting this into your config local opts = { silent = true, noremap = true, expr = true, replace_keycodes = false } keyset('i', '', 'coc#pum#visible() ? coc#pum#next(1) : v:lua.check_back_space() ? "" : coc#refresh()', opts) keyset('i', '', [[coc#pum#visible() ? coc#pum#prev(1) : "\"]], opts) -- Make to accept selected completion item or notify coc.nvim to format -- u breaks current undo, please make your own choice keyset('i', '', [[coc#pum#visible() ? coc#pum#confirm() : "\u\\=coc#on_enter()\"]], opts) -- Use to trigger snippets keyset('i', '', '(coc-snippets-expand-jump)') -- Use to trigger completion keyset('i', '', 'coc#refresh()', { silent = true, expr = true }) -- Use `[g` and `]g` to navigate diagnostics -- Use `:CocDiagnostics` to get all diagnostics of current buffer in location list keyset('n', '[g', '(coc-diagnostic-prev)', { silent = true }) keyset('n', ']g', '(coc-diagnostic-next)', { silent = true }) -- GoTo code navigation keyset('n', 'gd', '(coc-definition)', { silent = true }) keyset('n', 'gy', '(coc-type-definition)', { silent = true }) keyset('n', 'gi', '(coc-implementation)', { silent = true }) keyset('n', 'gr', '(coc-references)', { silent = true }) -- Use K to show documentation in preview window function _G.show_docs() local cw = vim.fn.expand('') if vim.fn.index({ 'vim', 'help' }, vim.bo.filetype) >= 0 then vim.api.nvim_command('h ' .. cw) elseif vim.api.nvim_eval('coc#rpc#ready()') then vim.fn.CocActionAsync('doHover') else vim.api.nvim_command('!' .. vim.o.keywordprg .. ' ' .. cw) end end keyset('n', 'K', 'lua _G.show_docs()', { silent = true }) -- Highlight the symbol and its references on a CursorHold event(cursor is idle) vim.api.nvim_create_augroup('CocGroup', {}) vim.api.nvim_create_autocmd('CursorHold', { group = 'CocGroup', command = "silent call CocActionAsync('highlight')", desc = 'Highlight symbol under cursor on CursorHold' }) -- Symbol renaming keyset('n', 'rn', '(coc-rename)', { silent = true }) -- Formatting selected code keyset('x', 'f', '(coc-format-selected)', { silent = true }) keyset('n', 'f', '(coc-format-selected)', { silent = true }) -- Setup formatexpr specified filetype(s) vim.api.nvim_create_autocmd('FileType', { group = 'CocGroup', pattern = 'typescript,json', command = "setl formatexpr=CocAction('formatSelected')", desc = 'Setup formatexpr specified filetype(s).' }) -- Apply codeAction to the selected region -- Example: `aap` for current paragraph local opts = { silent = true, nowait = true } keyset('x', 'a', '(coc-codeaction-selected)', opts) keyset('n', 'a', '(coc-codeaction-selected)', opts) -- Remap keys for apply code actions at the cursor position. keyset('n', 'ac', '(coc-codeaction-cursor)', opts) -- Remap keys for apply source code actions for current file. keyset('n', 'as', '(coc-codeaction-source)', opts) -- Apply the most preferred quickfix action on the current line. keyset('n', 'qf', '(coc-fix-current)', opts) -- Remap keys for apply refactor code actions. keyset('n', 're', '(coc-codeaction-refactor)', { silent = true }) keyset('x', 'r', '(coc-codeaction-refactor-selected)', { silent = true }) keyset('n', 'r', '(coc-codeaction-refactor-selected)', { silent = true }) -- Run the Code Lens actions on the current line keyset('n', 'cl', '(coc-codelens-action)', opts) -- Map function and class text objects -- NOTE: Requires 'textDocument.documentSymbol' support from the language server keyset('x', 'if', '(coc-funcobj-i)', opts) keyset('o', 'if', '(coc-funcobj-i)', opts) keyset('x', 'af', '(coc-funcobj-a)', opts) keyset('o', 'af', '(coc-funcobj-a)', opts) keyset('x', 'ic', '(coc-classobj-i)', opts) keyset('o', 'ic', '(coc-classobj-i)', opts) keyset('x', 'ac', '(coc-classobj-a)', opts) keyset('o', 'ac', '(coc-classobj-a)', opts) -- Remap and to scroll float windows/popups ---@diagnostic disable-next-line: redefined-local local opts = { silent = true, nowait = true, expr = true } keyset('n', '', 'coc#float#has_scroll() ? coc#float#scroll(1) : ""', opts) keyset('n', '', 'coc#float#has_scroll() ? coc#float#scroll(0) : ""', opts) keyset('i', '', 'coc#float#has_scroll() ? "=coc#float#scroll(1)" : ""', opts) keyset('i', '', 'coc#float#has_scroll() ? "=coc#float#scroll(0)" : ""', opts) keyset('v', '', 'coc#float#has_scroll() ? coc#float#scroll(1) : ""', opts) keyset('v', '', 'coc#float#has_scroll() ? coc#float#scroll(0) : ""', opts) -- Use CTRL-S for selections ranges -- Requires 'textDocument/selectionRange' support of language server keyset('n', '', '(coc-range-select)', { silent = true }) keyset('x', '', '(coc-range-select)', { silent = true }) -- Add `:Format` command to format current buffer vim.api.nvim_create_user_command('Format', "call CocAction('format')", {}) -- " Add `:Fold` command to fold current buffer vim.api.nvim_create_user_command('Fold', "call CocAction('fold', )", { nargs = '?' }) -- Add `:OR` command for organize imports of the current buffer vim.api.nvim_create_user_command('OR', "call CocActionAsync('runCommand', 'editor.action.organizeImport')", {}) -- Add (Neo)Vim's native statusline support -- NOTE: Please see `:h coc-status` for integrations with external plugins that -- provide custom statusline: lightline.vim, vim-airline vim.opt.statusline:prepend("%{coc#status()}%{get(b:,'coc_current_function','')}") -- Mappings for CoCList -- code actions and coc stuff ---@diagnostic disable-next-line: redefined-local local opts = { silent = true, nowait = true } -- Show all diagnostics keyset('n', 'a', ':CocList diagnostics', opts) -- Manage extensions keyset('n', 'e', ':CocList extensions', opts) -- Show commands keyset('n', 'c', ':CocList commands', opts) -- Find symbol of current document keyset('n', 'o', ':CocList outline', opts) -- Search workspace symbols keyset('n', 's', ':CocList -I symbols', opts) -- Do default action for next item keyset('n', 'j', ':CocNext', opts) -- Do default action for previous item keyset('n', 'k', ':CocPrev', opts) -- Resume latest coc list keyset('n', 'p', ':CocListResume', opts) ================================================ FILE: doc/coc-example-config.vim ================================================ " https://raw.githubusercontent.com/neoclide/coc.nvim/refs/heads/master/doc/coc-example-config.vim " May need for Vim (not Neovim) since coc.nvim calculates byte offset by count " utf-8 byte sequence set encoding=utf-8 " Some servers have issues with backup files, see #649 set nobackup set nowritebackup " Having longer updatetime (default is 4000 ms = 4s) leads to noticeable " delays and poor user experience set updatetime=300 " Always show the signcolumn, otherwise it would shift the text each time " diagnostics appear/become resolved set signcolumn=yes " Use tab for trigger completion with characters ahead and navigate " NOTE: There's always complete item selected by default, you may want to enable " no select by `"suggest.noselect": true` in your configuration file " NOTE: Use command ':verbose imap ' to make sure tab is not mapped by " other plugin before putting this into your config inoremap \ coc#pum#visible() ? coc#pum#next(1) : \ CheckBackspace() ? "\" : \ coc#refresh() inoremap coc#pum#visible() ? coc#pum#prev(1) : "\" " Make to accept selected completion item or notify coc.nvim to format " u breaks current undo, please make your own choice inoremap coc#pum#visible() ? coc#pum#confirm() \: "\u\\=coc#on_enter()\" function! CheckBackspace() abort let col = col('.') - 1 return !col || getline('.')[col - 1] =~# '\s' endfunction " Use to trigger completion if has('nvim') inoremap coc#refresh() else inoremap coc#refresh() endif " Use `[g` and `]g` to navigate diagnostics " Use `:CocDiagnostics` to get all diagnostics of current buffer in location list nmap [g (coc-diagnostic-prev) nmap ]g (coc-diagnostic-next) " GoTo code navigation nmap gd (coc-definition) nmap gy (coc-type-definition) nmap gi (coc-implementation) nmap gr (coc-references) " Use K to show documentation in preview window nnoremap K :call ShowDocumentation() function! ShowDocumentation() if CocAction('hasProvider', 'hover') call CocActionAsync('doHover') else call feedkeys('K', 'in') endif endfunction " Highlight the symbol and its references when holding the cursor autocmd CursorHold * silent call CocActionAsync('highlight') " Symbol renaming nmap rn (coc-rename) " Formatting selected code xmap f (coc-format-selected) nmap f (coc-format-selected) augroup mygroup autocmd! " Setup formatexpr specified filetype(s) autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected') augroup end " Applying code actions to the selected code block " Example: `aap` for current paragraph xmap a (coc-codeaction-selected) nmap a (coc-codeaction-selected) " Remap keys for applying code actions at the cursor position nmap ac (coc-codeaction-cursor) " Remap keys for apply code actions affect whole buffer nmap as (coc-codeaction-source) " Apply the most preferred quickfix action to fix diagnostic on the current line nmap qf (coc-fix-current) " Remap keys for applying refactor code actions nmap re (coc-codeaction-refactor) xmap r (coc-codeaction-refactor-selected) nmap r (coc-codeaction-refactor-selected) " Run the Code Lens action on the current line nmap cl (coc-codelens-action) " Map function and class text objects " NOTE: Requires 'textDocument.documentSymbol' support from the language server xmap if (coc-funcobj-i) omap if (coc-funcobj-i) xmap af (coc-funcobj-a) omap af (coc-funcobj-a) xmap ic (coc-classobj-i) omap ic (coc-classobj-i) xmap ac (coc-classobj-a) omap ac (coc-classobj-a) " Remap and to scroll float windows/popups nnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\" nnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\" inoremap coc#float#has_scroll() ? "\=coc#float#scroll(1)\" : "\" inoremap coc#float#has_scroll() ? "\=coc#float#scroll(0)\" : "\" vnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\" vnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\" " Use CTRL-S for selections ranges " Requires 'textDocument/selectionRange' support of language server nmap (coc-range-select) xmap (coc-range-select) " Add `:Format` command to format current buffer command! -nargs=0 Format :call CocActionAsync('format') " Add `:Fold` command to fold current buffer command! -nargs=? Fold :call CocAction('fold', ) " Add `:OR` command for organize imports of the current buffer command! -nargs=0 OR :call CocActionAsync('runCommand', 'editor.action.organizeImport') " Add (Neo)Vim's native statusline support " NOTE: Please see `:h coc-status` for integrations with external plugins that " provide custom statusline: lightline.vim, vim-airline set statusline^=%{coc#status()}%{get(b:,'coc_current_function','')} " Mappings for CoCList " Show all diagnostics nnoremap a :CocList diagnostics " Manage extensions nnoremap e :CocList extensions " Show commands nnoremap c :CocList commands " Find symbol of current document nnoremap o :CocList outline " Search workspace symbols nnoremap s :CocList -I symbols " Do default action for next item nnoremap j :CocNext " Do default action for previous item nnoremap k :CocPrev " Resume latest coc list nnoremap p :CocListResume ================================================ FILE: doc/coc.txt ================================================ *coc-nvim.txt* NodeJS client for Vim & Neovim. Version: 0.0.82 Author: Qiming Zhao CONTENTS *coc-contents* Introduction |coc-introduction| Requirements |coc-requirements| Installation |coc-installation| File system watch |coc-filesystemwatch| Language server |coc-languageserver| Extensions |coc-extensions| Configuration |coc-configuration| Floating windows |coc-floating| LSP features |coc-lsp| Document |coc-document| Hover |coc-hover| Completion |coc-completion| Inline completion |coc-inlineCompletion| Diagnostics |coc-diagnostics| Pull diagnostics |coc-pullDiagnostics| Locations |coc-locations| Rename |coc-rename| Signature help |coc-signature| Inlay hint |coc-inlayHint| Format |coc-format| Code action |coc-code-actions| Document highlights |coc-document-highlights| Document colors |coc-document-colors| Document links |coc-document-links| Snippets |coc-snippets| Workspace |coc-workspace| Cursors |coc-cursors| Outline |coc-outline| Call hierarchy |coc-callHierarchy| Type hierarchy |coc-typeHierarchy| Semantic highlights |coc-semantic-highlights| Fold |coc-fold| Selection range |coc-selection-range| Code Lens |coc-code-lens| Linked editing |coc-linked-editing| Interface |coc-interface| Key mappings |coc-key-mappings| Variables |coc-variables| Environment variables |coc-environment-variables| Buffer variables |coc-buffer-variables| Window variables |coc-window-variables| Global variables |coc-global-variables| Functions |coc-functions| Commands |coc-commands| Autocmds |coc-autocmds| Highlights |coc-highlights| Tree |coc-tree| Tree mappings |coc-tree-mappings| Tree filter |coc-tree-filter| List |coc-list| List command |coc-list-command| List command options |coc-list-options| List configuration |coc-list-configuration| List mappings |coc-list-mappings| list sources |coc-list-sources| Dialog |coc-dialog| Dialog basic |coc-dialog-basic| Dialog confirm |coc-dialog-confirm| Dialog input |coc-dialog-input| Dialog menu |coc-dialog-menu| Dialog picker |coc-dialog-picker| Notification |coc-notification| Statusline integration |coc-status| Manual |coc-status-manual| Airline |coc-status-airline| Lightline |coc-status-lightline| Create plugins |coc-plugins| FAQ |coc-faq| Change log |coc-changelog| ============================================================================== INTRODUCTION *coc-introduction* Coc.nvim enhances your (Neo)Vim to match the user experience provided by VSCode through a rich extension ecosystem and implemented the client features specified by Language Server Protocol (3.17 for now), see |coc-lsp|. Some features (like completion and inline completion) automatically works by default, all of them can be disabled by |coc-configuration|. Some key features: ~ • Typescript APIs compatible with both Vim8 and Neovim. • Loading VSCode-like extensions |coc-api-extension|. • Configuring coc.nvim and its extensions with JSON configuration |coc-configuration|. • Configuring Language Servers that using Language Server Protocol (LSP) |coc-config-languageserver|. It is designed for best possible integration with other Vim plugins. Note: coc.nvim doesn't come with support for any specific language. You will need to install coc.nvim extensions |coc-extensions| or set up the language server by use |coc-config-languageserver|. Note: multiple language servers for same document is allowed, but you should avoid configure same language server that already used by coc.nvim extension. Note: automatic completion plugins can't play nicely together, you can disable automatic completion of coc.nvim by use `"suggest.autoTrigger": "none"` (or `"suggest.autoTrigger": "trigger"`) in your |coc-configuration|. ============================================================================== REQUIREMENTS *coc-requirements* Neovim >= 0.8.0 or Vim >= 9.0.0483. NodeJS https://nodejs.org/ >= 16.18.0. For neovim user, use command |:checkhealth| to check issue with current environment. ============================================================================== INSTALLATION *coc-installation* If you're using [vim-plug](https://github.com/junegunn/vim-plug), add this to your `init.vim` or `.vimrc`: > Plug 'neoclide/coc.nvim', {'branch': 'release'} < And run: > :PlugInstall For other plugin managers, make sure to use the release branch (unless you need to build from typescript source code). To use Vim's native |packages| on Linux or macOS, use script like: > #!/bin/sh # for vim8 mkdir -p ~/.vim/pack/coc/start cd ~/.vim/pack/coc/start curl --fail -L https://github.com/neoclide/coc.nvim/archive/release.tar.gz|tar xzfv - vim -c 'helptags ~/.vim/pack/coc/start/doc|q' # for neovim mkdir -p ~/.local/share/nvim/site/pack/coc/start cd ~/.local/share/nvim/site/pack/coc/start curl --fail -L https://github.com/neoclide/coc.nvim/archive/release.tar.gz|tar xzfv - nvim -c 'helptags ~/.local/share/nvim/site/pack/coc/start|q' when using source code of coc.nvim, you'll have to run `npm install` in project root of coc.nvim. ============================================================================== File system watch *coc-filesystemwatch* Watchman https://facebook.github.io/watchman/ is used by coc.nvim to provide file change detection to extensions and languageservers. The watchman command is detected from your `$PATH`, the feature will silently fail when watchman can't work. Watchman automatically watch |coc-workspace-folders| for file events by default. Use command: > :CocCommand workspace.showOutput watchman < to open output channel of watchman. Use configuration |coc-config-fileSystemWatch| to change behavior of file system watch. Note: The default filesystem watch limit can be easily exceeded for many projects, checkout https://facebook.github.io/watchman/docs/install#system-specific-preparation ============================================================================== LANGUAGESERVER *coc-languageserver* Language servers are services which provide LSP features, the servers are provided by |coc-extensions| or |coc-config-languageserver|. To get language server for your language, see: https://github.com/neoclide/coc.nvim/wiki/Language-servers To debug language server, see: https://github.com/neoclide/coc.nvim/wiki/Debug-language-server ============================================================================== EXTENSIONS *coc-extensions* Compare to |coc-config-languageserver| extensions are more powerful since they could contribute json schemes, commands, and use middleware methods of languageserver to provide better results. Extensions could provide more features by make use of NodeJS and coc.nvim's API. *coc-extensions-folder* Extensions are loaded from `"extensions"` folder inside |coc#util#get_data_home()| and folders in 'runtimepath' when detected. Use `let $COC_NO_PLUGINS = '1'` in vimrc to disable the load of extensions. See |coc-api-extension| for the guide to create coc.nvim extension. Install extensions from git (not recommended): ~ • Download the source code. • In project root, install dependencies and compile the code by `npm install` (needed by most coc extensions). • Add the project root to vim's runtimepath by `set runtimepath^=/path/to/project` Plugin manager like [vim-plug] can be used as well. Note: use coc.nvim extensions from source code requires install dependencies, which may take huge disk usage. *coc-extensions-npm* Install global extensions from npm (recommended): ~ Use |:CocInstall| to install coc extensions from vim's command line. To make coc.nvim install extensions on startup, use |g:coc_global_extensions|. To use package manager other than npm (like `yarn` or `pnpm`), use |coc-config-npm-binPath|. To customize npm registry for coc.nvim add `coc.nvim:registry` in your `~/.npmrc`, like: > coc.nvim:registry=https://registry.mycompany.org/ < To customize extension folder, configure |g:coc_data_home|. Uninstall global extensions: ~ Use |:CocUninstall|. Update global extensions: ~ Use |:CocUpdate| or |:CocUpdateSync|. To configure extension behavior, see |coc-config-extensions|. Manage extensions: ~ Use |coc-list-extensions| or |CocAction('extensionStats')| to get list of extensions. ============================================================================== CONFIGURATION *coc-configuration* The configuration of coc.nvim is stored in file named "coc-settings.json". Configuration properties are contributed by coc.nvim itself and coc.nvim extensions. See |coc-config| for builtin configurations. The configuration files are all in JSON format (with comment supported), it's recommended to enable JSON completion and validation by install the `coc-json` extension: > :CocInstall coc-json < To fix the highlight of comment, use: > autocmd FileType json syntax match Comment +\/\/.\+$+ < in your vimrc. Global configuration file: ~ Command |:CocConfig| will open (create when necessary) a user settings file in the folder returned by |coc#util#get_config_home()|. The user configuration value could be overwritten by API |coc#config()| or |g:coc_user_config|. The global configuration file can be created in another directory by setting |g:coc_config_home| in your vimrc like: > let g:coc_config_home = '/path/to/folder' Folder configuration file: ~ To create a local configuration file for a specific workspace folder, use |:CocLocalConfig| to create and open `.vim/coc-settings.json` in current workspace folder. Folder configuration would overwrite user configuration. Note: the configuration file won't work when the parent folder is not resolved as workspace folder, it's best practice to start vim inside workspace folder, see |coc-workspace-folders|. *coc-configuration-expand* Variables expands: ~ Variables would be expanded in string values of configuration, supported variables: • `${userHome}` the path of the user's home folder • `${cwd}` current working directory of vim. You can also reference environment variables through the `${env:name}` syntax (for example, `${env:USERNAME}`), no expand happens when env not exists. Configurations that requires file paths (ex: |coc-config-workspace-ignoredFolders|) support expand `~` at the beginning of the filepath to user's home and some additional variables: • `${workspaceFolder}` the current opened file's workspace folder. • `${workspaceFolderBasename}` the name of the workspace folder opened in coc.nvim without any slashes (/). • `${file}` the current opened file. • `${fileDirname}` the current opened file's dirname. • `${fileExtname}` the current opened file's extension. • `${fileBasename}` the current opened file's basename • `${fileBasenameNoExtension}` the current opened file's basename with no file extension. *coc-configuration-scope* Configuration scope: ~ A configuration could be one of three different configuration scopes: • `"application"` the configuration could only be used in user configuration file. • `"resource"` the configuration could be used in user and workspace folder configuration file. • `"language-overridable"` the configuration could be used in user and workspace folder configuration file, and can be use used in language scoped configuration section like `[typescript][json]`. For example: > // disable inlay hint for some languages "[rust][lua][c]": { "inlayHint.enable": false } < ============================================================================== FLOATING WINDOWS *coc-floating* Floating windows/popups are created by |api-floatwin| on neovim or |popupwin| on vim. *coc-floating-scroll* Scroll floating windows: ~ See |coc#float#has_scroll()| for example. Note: use |coc#pum#scroll()| for scroll popup menu. *coc-floating-close* Close floating windows: ~ To close all floating windows/popups use |coc#float#close_all()| or |popup_clear()| on vim. Or you can use o on neovim which close all split windows as well. To close single floating window/popup, use |coc#float#close()|. *coc-floating-focus* Focus floating windows: ~ On neovim, use w (or |(coc-float-jump)|) could focus a floating window just created (if it's focusable). It's not allowed to focus popups on vim, unless it's using a terminal buffer. *coc-floating-config* Configure floating windows: ~ To set custom window options on floating window create, use autocmd |CocOpenFloat| or |CocOpenFloatPrompt|. Related variables: ~ • |g:coc_last_float_win| • |g:coc_borderchars| • |g:coc_border_joinchars| • |g:coc_markdown_disabled_languages| Related highlight groups: ~ • |CocFloating| For floating window background. • |CocFloatBorder| Default border highlight group of floating window. • |CocFloatDividingLine| For dividing lines. • |CocFloatActive| For active parts. • |CocMenuSel| For selected line. To customize floating windows used by popup menu, use: • |coc-config-suggest-floatConfig| • |coc-config-suggest-pumFloatConfig| For floating windows created around cursor, like diagnostics, hover and signature use |coc-config-floatFactory-floatConfig| for common float configurations. For further customization, use: • |coc-config-diagnostic-floatConfig| • |coc-config-signature-floatConfig| • |coc-config-hover-floatConfig| For customize dialog windows, use |coc-config-dialog|. For customize notification windows, use |coc-config-notification|. Configure |coc-preferences-enableMessageDialog| to show user non-interactive or interactive messages as notifications. To enable a more flexible user interaction with the messages check |coc-preferences-messageDialogKind| and |coc-preferences-messageReportKind| Use |coc-preferences-messageDialogKind| to configure how interactive messages which require some user input are shown to the user this configuration supersedes the |coc-preferences-enableMessageDialog|. Use |coc-preferences-messageReportKind| to configure how regular non-interactive messages are reported to the user, that will tell coc how to show regular plain text messages to the user. ============================================================================== LSP FEATURES *coc-lsp* Most features of LSP 3.17 are supported, checkout the specification at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ Features not supported: ~ • Telemetry. • Inline values for debugger. • Notebook document. LSP features only works with attached documents, see |coc-document-attached|. To check exists providers of current buffer, use command `:CocCommand document.checkBuffer` or API |CocHasProvider()|. For historic reason, some features automatically works by default, but some are not. Features automatically work by default: ~ • Trigger completion after text insert |coc-completion|. • Trigger inline completion |coc-inlineCompletion| • Diagnostics refresh |coc-diagnostics|. • Pull diagnostics |coc-pullDiagnostics|. • Trigger signature help |coc-signature|. • Inlay hints |coc-inlayHint| Most features could be toggled by |coc-configuration| and some vim variables. To disable all features that automatically work, use configuration: > "suggest.autoTrigger": "none", "diagnostic.enable": false, "pullDiagnostic.onChange": false, "signature.enable": false, "inlayHint.enable": false, < Features require enabled by configuration: ~ • Semantic highlights |coc-semantic-highlights|. • Document color highlights |coc-document-colors|. • Code lens, |coc-code-lens| • Linked editing, |coc-linked-editing|. • Format on type, enabled by |coc-preferences-formatOnType| • Format on save, enabled by |coc-preferences-formatOnSave|. Features requested by user: ~ • Locations related (including definitions, references etc.) |coc-locations| • Invoke code action |coc-code-actions|. • Show call hierarchy tree |coc-callHierarchy|. • Show type hierarchy tree |coc-typeHierarchy| • Format, range format and on type format |coc-format|. • Highlight same symbol ranges |coc-document-highlights|. • Outline of document symbols |coc-outline| and |coc-list-symbols|. • Show hover information |coc-hover|. • Rename symbol under cursor |coc-rename|. • Open link under cursor |coc-document-links|. • Selection range |coc-selection-range| • Create folding ranges |coc-fold|. For convenient, some actions have associated |coc-key-mappings| provided. Prefer |CocAction()| for more options. Features triggered by languageserver: ~ • Show message notification (use |coc-notification|). • Show message request (use |coc-dialog-menu|). • Log message notification (use `:CocCommand workspace.showOutput` to show output). • Show document request (opened by vim or your browser for url). • Work done progress (use |coc-notification|). To make coc.nvim provide LSP features for your languages, checkout https://github.com/neoclide/coc.nvim/wiki/Language-servers To debug issues with languageserver, checkout https://github.com/neoclide/coc.nvim/wiki/Debug-language-server ------------------------------------------------------------------------------ DOCUMENT *coc-document* An associated document is created on buffer create, and disposed on buffer unload. Attached document: ~ *coc-document-attached* An attached document means coc.nvim synchronize the lines of vim's buffer with associated document automatically. Only attached documents are synchronized with language servers and therefore LSP features could be provided for the attached buffer. The buffer may not be attached by following reasons: • The 'buftype' is neither nor 'acwrite', (could be bypassed by |b:coc_force_attach|). • Buffer variable |b:coc_enabled| is `0`. • Byte length of buffer exceed |coc-preferences-maxFileSize|. • Buffer is used for command line window. Use |CocAction('ensureDocument')| or `:CocCommand document.checkBuffer` to check attached state of current buffer. Filetype map: ~ *coc-document-filetype* Some filetypes are mapped to others to match the languageId used by VSCode, including: • javascript.jsx -> javascriptreact • typescript.jsx -> typescriptreact • typescript.tsx -> typescriptreact • tex -> latex Use |g:coc_filetype_map| to create additional filetype maps. Use `:CocCommand document.echoFiletype` to echo mapped filetype of current document. Note: make sure use mapped filetypes for configurations that expect filetypes. ------------------------------------------------------------------------------ HOVER *coc-hover* Hover feature provide information at a given text document position, normally include type information and documentation of current symbol. Hover functions: ~ • |CocAction('doHover')| Show hover information at cursor position. • |CocAction('definitionHover')||| Show hover information with definition context at cursor position. • |CocAction('getHover')| Get hover documentations at cursor position. *coc-hover-example* Hover key-mapping example: ~ > nnoremap K :call ShowDocumentation() " Show hover when provider exists, fallback to vim's builtin behavior. function! ShowDocumentation() if CocAction('hasProvider', 'hover') call CocActionAsync('definitionHover') else call feedkeys('K', 'in') endif endfunction < ------------------------------------------------------------------------------ COMPLETION *coc-completion* Vim's builtin completion is not used. The default completion works like completion in VSCode: • Completion is automatically triggered by default. • Selection is enabled by default, use |coc-config-suggest-noselect| to disable default selection. • When selection is enabled and no preselect item exists, the first complete item will be selected (depends on |coc-config-suggest-selection|). • Snippet expand and additional edits only work after confirm completion |coc#pum#confirm()|. • |'completeopt'| is not used and APIs of builtin popupmenu not work. *coc-completion-default* Default Key-mappings: ~ To make the new completion works like the builtin completion, without any additional configuration, the following key-mappings are used when the {lhs} is not mapped: • `` navigate to next complete item or inline complete item. • `` navigate to previous complete item or inline complete item. • `` navigate to next complete item (without word insert) or inline complete item. • `` navigate to previous complete item (without word insert) or inline complete item. • `` cancel the completion or inline completion. • `` confirm completion or accept current inline complete item. Use and to scroll: > inoremap coc#pum#visible() ? coc#pum#scroll(1) : "\" inoremap coc#pum#visible() ? coc#pum#scroll(0) : "\" < Note: and are not remapped by coc.nvim. *coc-completion-variables* Related variables: ~ • Disable completion for buffer: |b:coc_suggest_disable| • Disable specific sources for buffer: |b:coc_disabled_sources| • Disable words for completion: |b:coc_suggest_blacklist| • Add additional keyword characters: |b:coc_additional_keywords|, the buffer keyword characters are used for filter completion items instead of trigger completion, see |'iskeyword'|. *coc-completion-functions* Related functions: ~ • Trigger completion with options: |coc#start()|. • Trigger completion refresh: |coc#refresh()|. • Select and confirm completion: |coc#pum#select_confirm()|. • Check if the custom popupmenu is visible: |coc#pum#visible()|. • Select the next completion item: |coc#pum#next()|. • Select the previous completion item: |coc#pum#prev()|. • Cancel completion and reset trigger text: |coc#pum#cancel()|. • Confirm completion: |coc#pum#confirm()|. • Close the popupmenu only: |coc#pum#stop()|. • Get information about the popupmenu: |coc#pum#info()|. • Select specific completion item: |coc#pum#select()|. • Insert word of selected item and finish completion: |coc#pum#insert()|. • Insert one more character from current complete item: |coc#pum#one_more()|. • Scroll popupmenu: |coc#pum#scroll()|. *coc-completion-customize* Customize completion: ~ Use |coc-config-suggest| to change the completion behavior. Use |'pumwidth'| for configure the minimal width of the popupmenu and |'pumheight'| for its maximum height. Related Highlight groups: |CocPum| for highlight groups of customized pum. |CocSymbol| for kind icons. |CocMenuSel| for background highlight of selected item. |CocPumVirtualText| for virtual text when enabled by |coc-config-suggest-virtualText|. Note: background, border, title and winblend are configured by |coc-config-suggest-floatConfig|. Example user key-mappings: ~ *coc-completion-example* Note: use command `:verbose imap` to check current insert key-mappings when your key-mappings not work. Use and to navigate completion list: > function! CheckBackspace() abort let col = col('.') - 1 return !col || getline('.')[col - 1] =~ '\s' endfunction " Insert when previous text is space, refresh completion if not. inoremap \ coc#pum#visible() ? coc#pum#next(1): \ CheckBackspace() ? "\" : \ coc#refresh() inoremap coc#pum#visible() ? coc#pum#prev(1) : "\" Use to trigger completion: > if has('nvim') inoremap coc#refresh() else inoremap coc#refresh() endif < Use to confirm completion, use: > inoremap coc#pum#visible() ? coc#pum#select_confirm() : "\" < To make to confirm selection of selected complete item or notify coc.nvim to format on enter, use: > inoremap coc#pum#visible() ? coc#pum#select_confirm() \: "\u\\=coc#on_enter()\" Map for trigger completion, completion confirm, inline completion accept, snippet expand and jump like VSCode: > inoremap \ coc#pum#visible() ? coc#pum#select_confirm() : \ coc#inline#visible() ? coc#inline#accept() : \ coc#expandableOrJumpable() ? \ "\=coc#rpc#request('doKeymap', ['snippets-expand-jump',''])\" : \ CheckBackspace() ? "\" : \ coc#refresh() function! CheckBackspace() abort let col = col('.') - 1 return !col || getline('.')[col - 1] =~# '\s' endfunction let g:coc_snippet_next = '' < Note: the `coc-snippets` extension is required for this to work. ------------------------------------------------------------------------------ INLINE COMPLETION *coc-inlineCompletion* Inline Completion is a smart, lightweight alternative to traditional IntelliSense, offering faster, context-aware suggestions without disrupting your workflow. Inline completion is automatically triggered after document contents synchronize on insert mode by default. Use command `:CocCommand document.checkInlineCompletion` to check inline completion feature of current buffer. *coc-inlineCompletion-default* Default Key-mappings: ~ Inline completion and default completion shares default key mappings for navigate and finish actions. see |coc-completion-default|. To accept inline completion when pum is visible, finish the completion first or use |coc#inline#accept()|. *coc-inlineCompletion-functions* Related functions: ~ • Trigger inline completion: |coc#inline#trigger()|. • Check if inline completion visual text exists: |coc#inline#visible()|. • Cancel inline completion: |coc#inline#cancel()|. • Accept inline completion: |coc#inline#accept()|. • Navigate to next: |coc#inline#next()|. • Navigate to previous: |coc#inline#prev()|. *coc-inlineCompletion-customize* Customize completion: ~ Use |coc-config-inlineSuggest| to change the inline completion behavior. To disable inline completion for special buffers, use language overridable configuration in coc-settings.json like: > "[javascript][typescript]": { "inlineSuggest.autoTrigger": false } < Which disable auto trigger only, or use |b:coc_inline_disable| to disable trigger inline completion completely. Related Highlight groups: |CocInlineVirtualText| for virtual text highlight. |CocInlineAnnotation| for annotation highlight. ------------------------------------------------------------------------------ DIAGNOSTICS SUPPORT *coc-diagnostics* Diagnostics of coc.nvim are automatically refreshed to UI by default, checkout |coc-config-diagnostic| for available configurations. Note most language servers only send diagnostics for opened buffers for performance reason, some lint tools could provide diagnostics for all files in workspace. See |coc-highlights-diagnostics| for diagnostic related highlight groups. *coc-diagnostics-refresh* Changes on diagnostics refresh ~ • Add highlights for diagnostic ranges and virtual text (when enabled on neovim or vim >= 9.0.0067), see |coc-highlights-diagnostics|. • Add diagnostic signs to 'signcolumn', use `set signcolumn=yes` to avoid unnecessary UI refresh. • Update variable |b:coc_diagnostic_info|. • Refresh related |location-list| which was opened by |:CocDiagnostics|. Diagnostics are not refreshed when buffer is hidden, and refresh on insert mode is disabled by default. See |coc-highlights-diagnostics| for highlight groups used by diagnostics. *coc-diagnostics-toggle* Enable and disable diagnostics ~ Use |coc-config-diagnostic-enable| to toggle diagnostics feature. Use |CocAction('diagnosticToggle')| for enable/disable diagnostics feature. Use |CocAction('diagnosticToggleBuffer')| for enable/disable diagnostics of current buffer. Show diagnostic messages ~ Diagnostic messages would be automatically shown/hide when the diagnostics under cursor position changed (use float window/popup when possible) by default. To manually refresh diagnostics messages, use |(coc-diagnostic-info)| and |CocAction('diagnosticPreview')|. *coc-diagnostics-jump* Jump between diagnostics ~ Use key-mappings: |(coc-diagnostic-next)| jump to diagnostic after cursor position. |(coc-diagnostic-prev)| jump to diagnostic before cursor position. |(coc-diagnostic-next-error)| jump to next error. |(coc-diagnostic-prev-error)| jump to previous error. Diagnostic may have related location, to jump to related location, use: > :CocCommand workspace.diagnosticRelated < Check diagnostics ~ Use |coc-list-diagnostics| to open |coc-list| with all available diagnostics. Use API |CocAction('diagnosticList')| to get list of all diagnostics. Use |:CocDiagnostics| to open vim's location list with diagnostics of current buffer. To automatically close the location list window, use autocmd |CocDiagnosticChange| with |CocAction('diagnosticList')|. ------------------------------------------------------------------------------ PULL DIAGNOSTICS SUPPORT *coc-pullDiagnostics* Diagnostics are pulled for visible documents when supported by languageserver. Pull for workspace diagnostics is also enabled by default. Document diagnostics are pulled on change by default, and can be configured to be pulled on save. Checkout |coc-config-pullDiagnostic| for related configurations. ------------------------------------------------------------------------------ LOCATIONS SUPPORT *coc-locations* There're different kinds of locations, including "definitions", "declarations", "implementations", "typeDefinitions" and "references". Key-mappings for invoke locations request ~ • |(coc-definition)| • |(coc-declaration)| • |(coc-implementation)| • |(coc-type-definition)| • |(coc-references)| • |(coc-references-used)| Error will be shown when the buffer not attached |coc-document-attached|. Message will be shown when no result found. Location jump behavior ~ When there's only one location returned, the location is opened by command specified by |coc-preferences-jumpCommand| ("edit" by default), context mark is added by |m'|, so you can jump back previous location by . When multiple locations returned, |coc-list-location| is opened for preview and other further actions. To use |coc-list-location| for single location as well, use |coc-locations-api| (instead key-mappings provided by coc.nvim). To change default options of |coc-list-location| or use other plugin for list of locations, see |g:coc_enable_locationlist|. To use vim's quickfix for locations, use configuration |coc-preferences-useQuickfixForLocations|. To use vim's tag list for definitions, use |CocTagFunc()|. *coc-locations-api* Related APIs ~ • |CocAction('jumpDefinition')| Jump to definition locations. • |CocAction('jumpDeclaration')| Jump to declaration locations. • |CocAction('jumpImplementation')| Jump to implementation locations. • |CocAction('jumpTypeDefinition')| Jump to type definition locations. • |CocAction('jumpReferences')|| Jump to references. • |CocAction('jumpUsed')| Jump to references without declarations. • |CocAction('definitions')| Get definition list. • |CocAction('declarations')| Get declaration list. • |CocAction('implementations')| Get implementation list. • |CocAction('typeDefinitions')| Get type definition list. • |CocAction('references')| Get reference list. Send custom locations request to languageserver: • |CocLocations()| • |CocLocationsAsync()| ------------------------------------------------------------------------------ RENAME *coc-rename* Rename provides workspace-wide rename of a symbol. Workspace edit |coc-workspace-edit| is requested and applied to related buffers when confirmed. Check if current buffer has rename provider with `:echo CocAction('hasProvider', 'rename')` Rename key-mappings: ~ • |(coc-rename)| Rename functions: ~ • |CocAction('rename')| Rename the symbol under the cursor. • |CocAction('refactor')| Open refactor buffer for all references (including definitions), recommended for function signature refactor. Rename local variable: ~ Use command `:CocCommand document.renameCurrentWord` which uses |coc-cursors| to edit multiple locations at the same time and defaults to word extraction when rename provider doesn't exist. Rename configuration: ~ Use |coc-preferences-renameFillCurrent| to enable/disable populating prompt window with current variable name. ------------------------------------------------------------------------------ SIGNATURE HELP *coc-signature* Signature help for functions is shown automatically when user types trigger characters defined by the provider, which will use floating window/popup to show relevant documentation. Use |CocAction('showSignatureHelp')| to trigger signature help manually. Note: error will not be thrown when provider does not exist or nothing is returned by languageserver, use `echo CocAction('hasProvider', 'signature')` to check if a signature help provider exists. Use |coc-config-signature| to change default signature help behavior. |CocFloatActive| is used to highlight activated parameter part. ------------------------------------------------------------------------------ INLAY HINT *coc-inlayHint* Inlay hint is enabled for all filetypes by default. Inlay hint uses virtual text feature of vim. Vim9 or neovim >= 0.10 required to insert the virtual text at correct position. Note: you may need configure extension or languageserver to make inlay hint works. To temporarily toggle inlay hint specific buffer, use command: > :CocCommand document.enableInlayHint {bufnr} :CocCommand document.disableInlayHint {bufnr} :CocCommand document.toggleInlayHint {bufnr} < current bufnr is used when `{bufnr}` not specified. Change highlight group: ~ • |CocInlayHint| • |CocInlayHintType| • |CocInlayHintParameter| Configure inlay hint support: ~ |coc-config-inlayHint| ------------------------------------------------------------------------------ FORMAT *coc-format* Some tools may reload buffer from disk file during format, coc.nvim only apply `TextEdit[]` to the document. Don't be confused with vim's indent feature, configure/fix the 'indentexpr' of your buffer if the indent is wrong after character insert. (use |coc-format-ontype| might helps with the indent) *coc-format-options* Format options: ~ Buffer options that affect document format: 'eol', 'shiftwidth' and 'expandtab'. • |b:coc_trim_trailing_whitespace| Trim trailing whitespace on a line. • |b:coc_trim_final_newlines| Trim all newlines after the final newline at the end of the file. Those options are converted to `DocumentFormattingOptions` and transferred to languageservers before format. Note: the languageservers may only support some of those options. *coc-format-document* Format full document: ~ Choose "editor.action.formatDocument" from |coc-list-commands|. Or use |CocAction('format')|, you can create a command like: > command! -nargs=0 Format :call CocActionAsync('format') < to format current buffer. *coc-format-ontype* Format on type: ~ Format on type could be enabled by |coc-preferences-formatOnType|. Use `:CocCommand document.checkBuffer` to check if `formatOnType` provider exists for current buffer. To format on , create key-mapping of that uses |coc#on_enter()|. If you don't like the behavior on type bracket characters, configure |coc-preferences-bracketEnterImprove||. *coc-format-selected* Format selected code: ~ Use 'formatexpr' for specific filetypes: > autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected') So that |gq| could works for format range of lines. > Setup visual mode and operator key-mappings: > xmap f (coc-format-selected) nmap f (coc-format-selected) < *coc-format-onsave* Format on save: ~ To enable format on save, use configuration |coc-preferences-formatOnSave|. Or create |BufWritePre| autocmd like: > autocmd BufWritePre * call CocAction('format') < Note the operation have to synchronized, avoid use |CocActionAsync()|. Note to skip the autocmd, use `:noa w` to save the buffer. The operation on save will block your vim, to not block too long time, the operation will be canceled after 0.5s, configured by |coc-preferences-willSaveHandlerTimeout| ------------------------------------------------------------------------------ CODE ACTION *coc-code-actions* Code actions are used for ask languageserver to provide specific kind code changes. Possible code action kinds: • `quickfix` used for fix diagnostic(s). • `refactor` used for code refactor. • `source` code actions apply to the entire file. • `organizeImport` organize import statements of current document. Key-mappings for code actions: ~ • |(coc-fix-current)| Invoke quickfix action at current line if any. • |(coc-codeaction-cursor)| Choose code actions at cursor position. • |(coc-codeaction-line)| Choose code actions at current line. • |(coc-codeaction)| Choose code actions of current file. • |(coc-codeaction-source)| Choose source code action of current file. • |(coc-codeaction-selected)| Choose code actions from selected range. • |(coc-codeaction-refactor)| Choose refactor code action at cursor position. • |(coc-codeaction-refactor-selected)| Choose refactor code action with selected code. Except for |(coc-fix-current)| which invoke code action directly, |coc-dialog-menu| would be shown for pick specific code action. To invoke organize import action, use command like: > command! -nargs=0 OR :call CocAction('organizeImport') See |CocAction('organizeImport')| for details. Related APIs ~ • |CocAction('codeActions')| • |CocAction('organizeImport')| • |CocAction('fixAll')| • |CocAction('quickfixes')| • |CocAction('doCodeAction')| • |CocAction('doQuickfix')| • |CocAction('codeActionRange')| ------------------------------------------------------------------------------ DOCUMENT HIGHLIGHTS *coc-document-highlights* Document highlights is used for highlight same symbols of current document under cursor. To enable highlight on CursorHold, create an autocmd like this: > autocmd CursorHold * call CocActionAsync('highlight') < See |coc-config-documentHighlight| for related configurations. See |coc-highlights-document| for related highlight groups. Note error will not be thrown when provider not exists or nothing returned from languageserver with |CocAction('highlight')| Install `coc-highlight` extension if you want to highlight same words under cursor without languageserver support. To jump between previous/next symbol position, use `:CocCommand document.jumpToPrevSymbol` and `:CocCommand document.jumpToNextSymbol` ------------------------------------------------------------------------------ DOCUMENT COLORS *coc-document-colors* Document colors added color highlights to vim buffers. To enable document color highlights, use |coc-config-colors-enable|. Note: the highlights define gui colors only, make use you have 'termguicolors' enabled (and your terminal support gui colors) if you're using vim in terminal. To pick a color from system color picker, use |CocAction('pickColor')| or choose `editor.action.pickColor` from |:CocCommand|. Note: pick color may not work on your system. To change color presentation, use |CocAction('colorPresentation')| or choose `editor.action.colorPresentation` from |:CocCommand|. To toggle color highlight of current buffer, choose `document.toggleColors` from |:CocCommand| To highlights colors without languageservers, install https://github.com/neoclide/coc-highlight ============================================================================== DOCUMENT LINKS *coc-document-links* Check if current buffer have documentLink provider by `:echo CocAction('hasProvider', 'documentLink')` Highlight and tooltip of the links could be configured by |coc-config-links|. Use |coc-list-links| to manage list of links in current document. Document link key-mappings: ~ |(coc-openlink)| Document link functions: ~ • |CocAction('openLink')| Open link under cursor. • |CocAction('links')| Get link list of current buffer. ------------------------------------------------------------------------------ SNIPPETS SUPPORT *coc-snippets* Snippets engine of coc.nvim support both VSCode snippets and ultisnips snippets format. The completion items with snippet format has label ends with |coc-config-suggest-snippetIndicator| (`~` by default). Confirm the completion by |coc#pum#confirm()| or |coc#pum#select_confirm()| to expand the snippet and execute other possible actions of selected complete item. Jump snippet placeholders: ~ |g:coc_snippet_next| and |g:coc_snippet_prev| are used to jump placeholders on both select mode and insert mode, which defaults to and . Buffer key-mappings are created on snippet activate, and removed on snippet deactivate. Deactivate snippet session: ~ A snippet session would be deactivated under the following conditions: • The change affects the snippet and code outside. • Autocmd |InsertEnter| triggered outside snippet. • Jump to the final placeholder. Use |CocOpenLog| to checkout the cancel reason. Use |CocAction('snippetCancel')| for cancel snippet session manually. To load and expand custom snippets, install `coc-snippets` extension by command: > :CocInstall coc-snippets < Nested snippets: ~ Snippets can be nested, when jump to tabstop of parent snippet, it's not possible to jump back again (works like UltiSnip). Related configurations: ~ • |g:coc_snippet_prev| • |g:coc_snippet_next| • |g:coc_selectmode_mapping| • |coc-config-suggest-snippetIndicator| • |coc-config-suggest-preferCompleteThanJumpPlaceholder| • |coc-config-snippet-highlight| • |coc-config-snippet-statusText| • |coc-config-snippet-nextPlaceholderOnDelete| Related functions: ~ • |coc#snippet#next()| • |coc#snippet#prev()| • |coc#expandable()| • |coc#jumpable()| • |coc#expandableOrJumpable()| Related variables, highlights and autocmds: ~ • |g:coc_selected_text| Used for replace `${VISUAL}` and `${TM_SELECTED_TEXT}` placeholder of next expanded snippet. • |b:coc_snippet_active| Check if snippet session is activated. • |CocJumpPlaceholder| Autocmds triggered after placeholder jump. • |CocSnippetVisual| For highlight of current placeholders when the highlight is enabled. ------------------------------------------------------------------------------ WORKSPACE SUPPORT *coc-workspace* *coc-workspace-folders* Workspace folders ~ Unlike VSCode which prompt you to open folders, workspace folders of coc.nvim are resolved from filepath after document attached. A list of file/folder names is used for resolve workspace folder, the patterns could comes from: • |b:coc_root_patterns| • "rootPatterns" field of languageserver in |coc-config-languageserver|. • "rootPatterns" contributions from coc.nvim extensions. • |coc-config-workspace-rootPatterns| Workspace folder is resolved from cwd of vim first (by default) and then from top directory to the parent directory of current filepath, when workspace folder not resolved, current working directory is used if it's parent folder of current buffer. Configurations are provided to change the default behavior: • |coc-config-workspace-ignoredFiletypes| • |coc-config-workspace-ignoredFolders| • |coc-config-workspace-bottomUpFiletypes| • |coc-config-workspace-workspaceFolderCheckCwd| • |coc-config-workspace-workspaceFolderFallbackCwd| Note for performance reason, user's home directory would never considered as workspace folder, which also means the languageserver that requires workspace folder may not work when you start vim from home directory. To preserve workspace folders across vim session, |g:WorkspaceFolders| is provided. Use `:CocCommand workspace.workspaceFolders` to echo current workspaceFolders. To manage current workspace folders, use |coc-list-folders| To get related root patterns of current buffer, use |coc#util#root_patterns()| *coc-workspace-edits* Workspace edit ~ Workspace edit is used to apply changes for multiple buffers(and/or files), the edit could contains document edits and file operations (including file create, file/directory delete and file/directory rename). When the edit failed to apply, coc.nvim will revert the changes (including document edits and file operations) that previous made. Files not loaded would be loaded by `tab drop` command, configured by |coc-config-workspace-openResourceCommand|. To undo and redo workspace edit just applied, use command `:CocCommand workspace.undo` and `:CocCommand workspace.redo` To inspect previous workspace edit, use command `:CocCommand workspace.inspectEdit`, in opened buffer, use for jump to change position under cursor. *coc-workspace-rename* Rename current file ~ To move or rename current file, it's recommended to use `:CocCommand workspace.renameCurrentFile` which makes vim reload current buffer and send expected events to language servers. *coc-workspace-output* Output channels ~ Output channels shows logs for user to inspect. Use command `workspace.showOutput` to open output channel. Builtin channels: - `watchman` shows watchman related logs. - `extensions` shows extension install and update related logs. |coc-languageserver| and |coc-extensions| could contribute output channels. ------------------------------------------------------------------------------ CURSORS SUPPORT *coc-cursors* Multiple cursors supported is added to allow edit multiple locations at once. Cursors session could be started by following ways: • Use command `:CocCommand document.renameCurrentWord` to rename variable under cursor. • Use |(coc-refactor)| to open |coc-refactor-buffer|. • Use |:CocSearch| to open searched locations with |coc-refactor-buffer|. • Use cursors related key-mappings to add text range, including |(coc-cursors-operator)|, |(coc-cursors-word)|, |(coc-cursors-position)| and |(coc-cursors-range)| • Ranges added by command `editor.action.addRanges` from coc extensions. Default key-mappings when cursors activated: • cancel cursors session. • jump to next cursors range. • jump to previous cursors range. Use |coc-config-cursors| to change cursors related key-mappings. Use highlight group |CocCursorRange| to change default range highlight. Use |b:coc_cursors_activated| to check if cursors session is activated. *coc-refactor-buffer* Refactor buffer is a specific buffer with |coc-cursors| enabled, normally opened by |CocAction('refactor')| and |:CocSearch|. When the refactor buffer saved, related buffers and files would be changed at once, workspace edits would be applied, which could be undo and redo, see |coc-workspace-edits|. Check out |coc-config-refactor| for related configuration. Use to open buffer at current position in split window. Use to invoke tab open or remove action with current code chunk. -------------------------------------------------------------------------------- SYMBOLS OUTLINE *coc-outline* Outline is a split window with current document symbols rendered as |coc-tree|. To show and hide outline of current window, use |CocAction('showOutline')| and |CocAction('hideOutline')|. Outline view has |w:cocViewId| set to "OUTLINE". Following outline features are supported: • Start fuzzy filter by |coc-config-tree-key-activeFilter|. • Automatic update after document change. • Automatic reload when buffer in current window changed. • Automatic follow cursor position by default. • Different filter modes that can be changed on the fly |coc-config-outline-switchSortKey|. • Enable auto preview by |coc-config-outline-togglePreviewKey|. Outline would try to reload document symbols after 500ms when provider not registered, which avoid the necessary to check provider existence. Checkout |coc-config-tree| and |coc-config-outline| for available configurations. Checkout |CocTree| and |CocSymbol| for customize highlights. Use configuration `"suggest.completionItemKindLabels"` for custom icons. *coc-outline-example* To show outline for each tab automatically, use |autocmd|: > autocmd VimEnter,Tabnew * \ if empty(&buftype) | call CocActionAsync('showOutline', 1) | endif < To close outline when it's the last window automatically, use |autocmd| like: > autocmd BufEnter * call CheckOutline() function! CheckOutline() abort if &filetype ==# 'coctree' && winnr('$') == 1 if tabpagenr('$') != 1 close else bdelete endif endif endfunction < Create a key-mapping to toggle outline, like: > nnoremap o :call ToggleOutline() function! ToggleOutline() abort let winid = coc#window#find('cocViewId', 'OUTLINE') if winid == -1 call CocActionAsync('showOutline', 1) else call coc#window#close(winid) endif endfunction < -------------------------------------------------------------------------------- CALL HIERARCHY *coc-callHierarchy* A call hierarchy is a split |coc-tree| window with locations for incoming or outgoing calls of function under cursor position. Call hierarchy window is opened by |CocAction('showIncomingCalls')| and |CocAction('showOutgoingCalls')|. The tree view window has |w:cocViewId| set to "CALLS". Call hierarchy is configured by |CocSymbol|, |coc-config-callHierarchy| and |coc-config-tree|. Related ranges are highlighted with |CocSelectedRange| highlight group in opened buffer. |coc-dialog-menu| could be invoked by |coc-config-tree-key-actions| (default to ). Available actions: • Dismiss. • Open in new tab. • Show Incoming Calls. • Show Outgoing Calls. Use in call hierarchy tree to open location in original window. -------------------------------------------------------------------------------- TYPE HIERARCHY *coc-typeHierarchy* A type hierarchy is a split |coc-tree| window with locations for super types or sub types from types at current position. Type hierarchy window is opened by |CocAction('showSuperTypes')| and |CocAction('showSubTypes')|. The tree view window has |w:cocViewId| set to "TYPES". Type hierarchy is configured by |CocSymbol|, |coc-config-typeHierarchy| and |coc-config-tree|. Actions are the same as |coc-callHierarchy|. -------------------------------------------------------------------------------- SEMANTIC HIGHLIGHTS *coc-semantic-highlights* Semantic tokens are used to add additional color information to a buffer that depends on language specific symbol information. Use |coc-config-semanticTokens-enable| to enable semantic tokens highlights. Use `:CocCommand semanticTokens.checkCurrent` to check semantic highlight information with current buffer. To create custom highlights for symbol under cursor, follow these steps: • Inspect semantic token by > :CocCommand semanticTokens.inspect < to check token type and token modifiers with current symbol. • Create new highlight group by |highlight|, for example: > :hi link CocSemDeclarationVariable MoreMsg < • Refresh semantic highlight of current buffer by: > :CocCommand semanticTokens.refreshCurrent < • Clear semantic highlight by: > " Clear semantic tokens highlight of current buffer :CocCommand semanticTokens.clearCurrent " Clear semantic tokens highlight for all buffers :CocCommand semanticTokens.clearAll < See |CocSem| to customize semantic token highlight groups. See |coc-config-semanticTokens| for related configurations. -------------------------------------------------------------------------------- FOLD *coc-fold* Check if current buffer have fold provider by `:echo CocAction('hasProvider', 'foldingRange')` Use |CocAction('fold')| to create folds by request the languageserver and create manual folds on current window. -------------------------------------------------------------------------------- SELECTION RANGE *coc-selection-range* Select range forward or backward at cursor position. Check if current buffer have selection range provider by `:echo CocAction('hasProvider', 'selectionRange')` Selection range key-mappings: ~ • |(coc-range-select)| Select range forward. • |(coc-range-select-backward)| Select range backward. Selection range function: ~ • |CocAction('rangeSelect')| Visual select previous or next selection range -------------------------------------------------------------------------------- CODE LENS *coc-code-lens* Code lens feature shows additional information above or after specific lines. CodeLens are not shown by default, use |coc-config-codeLens-enable| to enable, you may also need enable codeLens feature by configure extension or languageserver. Check if current buffer have code lens provider by `:echo CocAction('hasProvider', 'codeLens')` To temporarily toggle codeLens of current buffer, use command `:CocCommand document.toggleCodeLens` To invoke command from codeLens, use |(coc-codelens-action)|. Use |CocCodeLens| for highlight of codeLens virtual text. Code lens are automatically requested on buffer create/change, checkout |coc-config-codeLens| for available configurations. -------------------------------------------------------------------------------- LINKED EDITING *coc-linked-editing* Linked editing feature enables editing multiple linked ranges at the same time, for example: html tags. The linked editing ranges would be highlighted with |CocLinkedEditing| when activated. Check if current buffer have linked editing provider by `:echo CocAction('hasProvider', 'linkedEditing')` Linked editing feature is disabled by default, use |coc-preferences-enableLinkedEditing| to enable. ============================================================================== INTERFACE *coc-interface* -------------------------------------------------------------------------------- Key mappings *coc-key-mappings* There're some cases that local key-mappings are enabled for current buffer. • Snippet jump key-mappings when snippet is activated: |g:coc_snippet_prev| and |g:coc_snippet_next|. • Cursor jump and cancel key-mappings when cursors is activated |coc-config-cursors|. • Dialog key-mappings for confirm and cancel dialog window |coc-config-dialog|. • Key-mappings for |CocList| buffer: |coc-list-mappings|. Note: Use |:verbose| command to check key-mappings that taking effect. Note: Use |noremap| with will make the key-mapping not work at all. Note: key-mappings are provided for convenient, use |CocActionAsync()| or |CocAction()| for more options. Normal mode key-mappings: ~ *(coc-diagnostic-info)* Show diagnostic message of current position by invoke |CocAction('diagnosticInfo')| *(coc-diagnostic-next)* Jump to next diagnostic position after current cursor position. *(coc-diagnostic-prev)* Jump to previous diagnostic position before current cursor position. *(coc-diagnostic-next-error)* Jump to next diagnostic error position. *(coc-diagnostic-prev-error)* Jump to previous diagnostic error position. *(coc-definition)* Jump to definition(s) of current symbol by invoke |CocAction('jumpDefinition')| *(coc-declaration)* Jump to declaration(s) of current symbol by invoke |CocAction('jumpDeclaration')| *(coc-implementation)* Jump to implementation(s) of current symbol by invoke |CocAction('jumpImplementation')| *(coc-type-definition)* Jump to type definition(s) of current symbol by invoke |CocAction('jumpTypeDefinition')| *(coc-references)* Jump to references of current symbol by invoke |CocAction('jumpReferences')| *(coc-references-used)* Jump to references of current symbol exclude declarations. *(coc-format-selected)* Format selected range, works on both |visual-mode| and |normal-mode|, when used in normal mode, the selection works on the motion object. For example: > vmap p (coc-format-selected) nmap p (coc-format-selected) < makes `p` format the visually selected range, and you can use `pap` to format a paragraph. *(coc-format)* Format the whole buffer by invoke |CocAction('format')| *(coc-rename)* Rename symbol under cursor to a new word by invoke |CocAction('rename')| *(coc-refactor)* Open refactor window for refactor of current symbol by invoke |CocAction('refactor')| *(coc-command-repeat)* Repeat latest |CocCommand|. *(coc-codeaction)* Get and run code action(s) for current file, use |coc-codeaction-cursor| for same behavior as VSCode. *(coc-codeaction-source)* Get and run source code action(s) for current file. The same as 'Source action...' in context menu of VSCode. *(coc-codeaction-line)* Get and run code action(s) for current line. *(coc-codeaction-cursor)* Get and run code action(s) using empty range at current cursor. *(coc-codeaction-selected)* Get and run code action(s) with the selected code. Works on both |visual-mode| and |normal-mode|. *(coc-codeaction-refactor)* Get and run refactor code action(s) at current cursor, the same as refactor context menu in VSCode, disabled actions are not excluded. *(coc-codeaction-refactor-selected)* Get and run refactor code action(s) with selected code. Works on both |visual-mode| and |normal-mode|. *(coc-openlink)* Open link under cursor by use |CocAction('openlink')|. *(coc-codelens-action)* invoke command contributed by codeLens at the current line. *(coc-fix-current)* Try first quickfix action for diagnostics of current line. *(coc-float-hide)* Hide all float windows/popups created by coc.nvim. *(coc-float-jump)* Jump to first float window (neovim only), use |CTRL-W_p| for jump to previous window. *(coc-range-select)* Select next selection range. Works on both |visual-mode| and |normal-mode|. Note: requires selection ranges feature of language server. *(coc-funcobj-i)* Select inside function. Recommend mapping: Works on both |visual-mode| and |normal-mode|. > xmap if (coc-funcobj-i) omap if (coc-funcobj-i) < Note: Requires 'textDocument.documentSymbol' support from the language server. *(coc-funcobj-a)* Select around function. Works on both |visual-mode| and |normal-mode|. Recommended mapping: > xmap af (coc-funcobj-a) omap af (coc-funcobj-a) < Note: Requires 'textDocument.documentSymbol' support from the language server. *(coc-classobj-i)* Select inside class/struct/interface. Works on both |visual-mode| and |normal-mode|. Recommended mapping: > xmap ic (coc-classobj-i) omap ic (coc-classobj-i) < Note: Requires 'textDocument.documentSymbol' support from the language server. *(coc-classobj-a)* Select around class/struct/interface. Works on both |visual-mode| and |normal-mode|. Recommended mapping: > xmap ac (coc-classobj-a) omap ac (coc-classobj-a) < Note: Requires 'textDocument.documentSymbol' support from the language server. *(coc-cursors-operator)* Add text to cursors session by motion object. *(coc-cursors-word)* Add current word to cursors session. *(coc-cursors-position)* Add current position as empty range to cursors session. Visual mode key-mappings: ~ *(coc-range-select-backward)* Select previous selection range. Note: requires selection ranges feature of language server, like: coc-tsserver, coc-python *(coc-cursors-range)* Add selection to cursors session. ============================================================================== VARIABLES *coc-variables* The Variables used by coc.nvim. -------------------------------------------------------------------------------- ENVIRONMENT VARIABLES *coc-environment-variables* $COC_NODE_PATH *$COC_NODE_PATH* Full path of NodeJS executable to start the node process, `node` command is used when the variable not exists. $VIMCONFIG Full path of directory which used for contain the user configuration file, not used when |g:coc_config_home| exists. $NODE_CLIENT_LOG_FILE Full path of client log file, used when |g:node_client_debug| enabled. $COC_VIM_CHANNEL_ENABLE *$COC_VIM_CHANNEL_ENABLE* Enable vim's channel log |ch_logfile()| when is "1". -------------------------------------------------------------------------------- BUFFER VARIABLES *coc-buffer-variables* b:coc_enabled *b:coc_enabled* Set to `0` on buffer create if you don't want coc.nvim receive content from buffer. Normally used with |BufRead| autocmd, example: > " Disable file with size > 1MB autocmd BufRead * if getfsize(expand('')) > 1024*1024 | \ let b:coc_enabled=0 | \ endif b:coc_disable_autoformat *b:coc_disable_autoformat* Set to `1` on buffer create if you don't want coc.nvim not format the buffer automatically (when typing or saving the buffer). Normally used with |BufRead| autocmd, example: > " Disable automatic format with file size > 1MB autocmd BufRead * if getfsize(expand('')) > 1024*1024 | \ let b:coc_disable_autoformat = 1 | \ endif < b:coc_force_attach *b:coc_force_attach* When is `1`, attach the buffer without check the 'buftype' option. Should be set on buffer create. b:coc_root_patterns *b:coc_root_patterns* Root patterns used for resolving workspaceFolder for the current file, will be used instead of `"workspace.rootPatterns"` setting. Example: > autocmd FileType python let b:coc_root_patterns = \ ['.git', '.env'] < b:coc_suggest_disable *b:coc_suggest_disable* Disable trigger completion of buffer. Example: > " Disable completion for python autocmd FileType python let b:coc_suggest_disable = 1 b:coc_inline_disable *b:coc_inline_disable* Disable trigger inline completion of buffer. Example: > " Disable inline completion for python autocmd FileType python let b:coc_inline_disable = 1 b:coc_disabled_sources *b:coc_disabled_sources* Disabled completion sources of current buffer. Example: > let b:coc_disabled_sources = ['around', 'buffer', 'file'] < b:coc_suggest_blacklist *b:coc_suggest_blacklist* List of input words for which completion should not be triggered. Example: > " Disable completion for 'end' in Lua files autocmd FileType lua let b:coc_suggest_blacklist = ["end"] b:coc_additional_keywords *b:coc_additional_keywords* Addition keyword characters for generate keywords. Example: > " Add keyword characters for CSS autocmd FileType css let b:coc_additional_keywords = ["-"] b:coc_trim_trailing_whitespace *b:coc_trim_trailing_whitespace* Trim trailing whitespace on a line, default `0`. Use by "FormattingOptions" send to the server. b:coc_trim_final_newlines *b:coc_trim_final_newlines* Trim all newlines after the final newline at the end of the file. Use by "FormattingOptions" send to the server. Other buffer options that affect document format: 'eol', 'shiftwidth' and 'expandtab'. Note: language server may not respect format options. -------------------------------------------------------------------------------- WINDOW VARIABLES *coc-window-variables* w:cocViewId *w:cocViewId* Exists with tree view window to identify the kind of tree view. Builtin in kinds: - 'OUTLINE' for outline tree view. - 'CALLS' for incoming calls and outgoing calls tree view. - 'TYPES' for type hierarchy tree view. Extensions could contribute other kinds of tree view. -------------------------------------------------------------------------------- GLOBAL VARIABLES *coc-global-variables* g:coc_disable_startup_warning *g:coc_disable_startup_warning* Disable possible warning on startup for old vim/node version. Default: 0 g:coc_disable_mappings_check *g:coc_disable_mappings_check* Disable completion key-mappings check for ``, ``, ``, ``. Default: 0 g:coc_disable_uncaught_error *g:coc_disable_uncaught_error* Disable uncaught error messages from node process of coc.nvim. Default: 0 g:coc_text_prop_offset *g:coc_text_prop_offset* Start |textprop| id offset of highlight namespaces on vim, change to other value to avoid conflict with other vim plugin. Default: 1000 g:coc_disable_transparent_cursor *g:coc_disable_transparent_cursor* Disable transparent cursor when CocList is activated. Set it to `1` if you have issue with transparent cursor. Default: 0 g:coc_start_at_startup *g:coc_start_at_startup* Start coc service on startup, use |CocStart| to start server when you set it to 0. Default: 1 g:coc_list_preview_filetype *g:coc_list_preview_filetype* Enable set filetype for buffer in the preview window of |CocList|. g:coc_global_extensions *g:coc_global_extensions* Global extension names to install when they aren't installed. > let g:coc_global_extensions = ['coc-json', 'coc-git'] < Note: coc.nvim will try to install extensions that are not installed in this list after initialization. g:coc_enable_locationlist *g:coc_enable_locationlist* Use location list of |coc-list| when jump to locations. Set it to 0 when you need customize behavior of location jump by use |CocLocationsChange| and |g:coc_jump_locations| If you want use vim's quickfix list instead, add `"coc.preferences.useQuickfixForLocations": true` in your configuration file, this configuration would be ignored and no |CocLocationsChange| triggered. Default: 1 g:coc_snippet_next *g:coc_snippet_next* Trigger key for going to the next snippet position, applied in insert and select mode. Only works when snippet session is activated. Default: g:coc_snippet_prev *g:coc_snippet_prev* Trigger key for going to the previous snippet position, applied in insert and select mode. Only works when snippet session is activated. Default: g:coc_selected_text *g:coc_selected_text* The text used for replace `${VISUAL}` and `${TM_SELECTED_TEXT}` placeholder of next expanded snippet. Normally created by press `(coc-snippets-select)` provided by coc-snippets extensions. The variable is removed after snippet expand. Not exists by default. g:coc_filetype_map *g:coc_filetype_map* Map for document filetypes so the server could handle current document as another filetype, example: > let g:coc_filetype_map = { \ 'html.swig': 'html', \ 'wxss': 'css', \ } < Default: {} See |coc-document-filetype| for details. g:coc_selectmode_mapping *g:coc_selectmode_mapping* Add key mappings for making snippet select mode easier. The same as |Ultisnip| does. > snoremap c snoremap c snoremap c snoremap "_c < Default: 1 g:coc_node_path *g:coc_node_path* Path to node executable to start coc service, example: > let g:coc_node_path = '/usr/local/opt/node@12/bin/node' < Use this when coc has problems with your system node, Note: you can use `~` as home directory. g:coc_node_args *g:coc_node_args* Arguments passed to node when starting coc.nvim service. Useful for start coc.nvim in debug mode, example: > > let g:coc_node_args = ['--nolazy', '--inspect-brk=6045'] < Default: [] g:coc_status_error_sign *g:coc_status_error_sign* Error character used by |coc#status()|, default: `E` g:coc_status_warning_sign *g:coc_status_warning_sign* Warning character used by |coc#status()|, default: `W` g:coc_quickfix_open_command *g:coc_quickfix_open_command* Command used for open quickfix list. To jump fist position after quickfix list opened, you can use: > let g:coc_quickfix_open_command = 'copen|cfirst' < Default: |copen| g:coc_open_url_command *g:coc_open_url_command* Command used for open remote url, when not exists, coc.nvim will try to use "open", "xdg-open" on Mac and Linux, "cmd /c start" on windows. g:node_client_debug *g:node_client_debug* Enable debug mode of node client for check rpc messages between vim and coc.nvim. Use environment variable `$NODE_CLIENT_LOG_FILE` to set the log file or get the log file after coc.nvim started. On vim9, this variable also enables vim's channel log, the log filepath would be disposed on the screen after vim start. To open the log file, use command: > :call coc#client#open_log() < Default: `0` g:coc_user_config *g:coc_user_config* User configuration which will be passed to coc.nvim process during initialization, no effect when changed after coc.nvim started. Prefer |coc#config| unless coc.nvim is lazy loaded. Example: > let g:coc_user_config = {} let g:coc_user_config['suggest.timeout'] = 500 let g:coc_user_config['suggest.noselect'] = v:true < Note: those configuration would overwrite the configuration from the user's settings file, unless you have to use some dynamic variables, using the settings file is recommended. g:coc_config_home *g:coc_config_home* Configure the directory which will be used to look for user's `coc-settings.json`, default: Windows: `~/AppData/Local/nvim` Other: `~/.config/nvim` g:coc_data_home *g:coc_data_home* Configure the directory which will be used to for data files(extensions, MRU and so on), default: Windows: `~/AppData/Local/coc` Other: `~/.config/coc` g:coc_terminal_height *g:coc_terminal_height* Height of terminal window, default `8`. g:coc_markdown_disabled_languages *g:coc_markdown_disabled_languages* Filetype list that should be disabled for highlight in markdown block, useful to disable filetypes that could be slow with syntax highlighting, example: > let g:coc_markdown_disabled_languages = ['html'] g:coc_highlight_maximum_count *g:coc_highlight_maximum_count* When highlight items exceed maximum count, highlight items will be grouped and added by using |timer_start| for better user experience. Default `500` g:coc_default_semantic_highlight_groups *g:coc_default_semantic_highlight_groups* Create default semantic highlight groups for |coc-semantic-highlights| Default: `1` g:coc_max_treeview_width *g:coc_max_treeview_width* Maximum width of tree view when adjusted by auto width. Default: `40` g:coc_borderchars *g:coc_borderchars* Border characters used by border window, default to: > ['─', '│', '─', '│', '┌', '┐', '┘', '└'] < Note: you may need special font like Nerd font to show them. g:coc_border_joinchars *g:coc_border_joinchars* Border join characters used by float window/popup, default to: > ['┬', '┤', '┴', '├'] < Note: you may need special font like Nerd font to show them. g:coc_prompt_win_width *g:coc_prompt_win_width* Width of input prompt window, default `32`. *g:coc_notify* g:coc_notify_error_icon *g:coc_notify_error_icon* Error icon for notification, default to:  g:coc_notify_warning_icon *g:coc_notify_warning_icon* Warning icon for notification, default to: ⚠ g:coc_notify_info_icon *g:coc_notify_info_icon* Info icon for notification, default to:  -------------------------------------------------------------------------------- Some variables are provided by coc.nvim. g:WorkspaceFolders *g:WorkspaceFolders* Current workspace folders, used for restoring from a session file, add `set sessionoptions+=globals` to vimrc for restoring globals on session load. g:coc_jump_locations *g:coc_jump_locations* This variable would be set to jump locations when the |CocLocationsChange| autocmd is fired. Each location item contains: 'filename': full file path. 'lnum': line number (1 based). 'col': column number(1 based). 'text': line content of location. g:coc_process_pid *g:coc_process_pid* Process pid of coc.nvim service. If your vim doesn't kill coc.nvim process on exit, use: > autocmd VimLeavePre * if get(g:, 'coc_process_pid', 0) \ | call system('kill -9 '.g:coc_process_pid) | endif < in your vimrc. g:coc_service_initialized *g:coc_service_initialized* Is `1` when coc.nvim initialized, used with autocmd |CocNvimInit|. g:coc_status *g:coc_status* Status string contributed by coc.nvim and extensions, used for status line. You may need to escape `%` for your status line plugin. VimL: `substitute(g:coc_status, '%', '%%', 'g')` Lua: `string.gsub(vim.g.coc_status, "%%", "%%%%")` g:coc_last_float_win *g:coc_last_float_win* Window id of latest created float/popup window. g:coc_last_hover_message *g:coc_last_hover_message* Last message echoed from `doHover`, can be used in statusline. Note: not used when floating or preview window used for `doHover`. b:coc_snippet_active *b:coc_snippet_active* Is `1` when snippet session is activated, use |coc#jumpable| to check if it's possible to jump placeholder. b:coc_diagnostic_disable *b:coc_diagnostic_disable* Disable diagnostic support of current buffer. b:coc_diagnostic_info *b:coc_diagnostic_info* Diagnostic information of current buffer, the format would look like: `{'error': 0, 'warning': 0, 'information': 0, 'hint':0}` can be used to customize statusline. See |coc-status|. b:coc_diagnostic_map *b:coc_diagnostic_map* Diagnostics of current buffer, the format is same as *diagnostic-structure* b:coc_current_function *b:coc_current_function* Function string that current cursor in. Enable |coc-preferences-currentFunctionSymbolAutoUpdate| to update the value on CursorMove and use |coc-preferences-currentFunctionSymbolDebounceTime| to set the time to debounce the event. b:coc_cursors_activated *b:coc_cursors_activated* Use expression `get(b:, 'coc_cursors_activated',0)` to check if cursors session is activated for current buffer. -------------------------------------------------------------------------------- FUNCTIONS *coc-functions* Some functions only work after the coc.nvim has been initialized. To run a function on startup, use an autocmd like: > autocmd User CocNvimInit call CocAction('runCommand', \ 'tsserver.watchBuild') < coc#start([{option}]) *coc#start()* Start completion with optional {option}. Option could contains: - `source` specific completion source name. - `col` the start column of completion (1 based). Example: > inoremap =coc#start({'source': 'word'}) < Use `:CocList sources` to get available sources. coc#refresh() *coc#refresh()* Start or refresh completion at current cursor position, bind this to 'imap' to trigger completion, example: > if has('nvim') inoremap coc#refresh() else inoremap coc#refresh() endif coc#config({section}, {value}) *coc#config()* Change user configuration, overwrite configurations from user config file and default values. Example: > call coc#config('coc.preferences', { \ 'willSaveHandlerTimeout': 1000, \}) call coc#config('languageserver', { \ 'ccls': { \ "command": "ccls", \ "trace.server": "verbose", \ "filetypes": ["c", "cpp", "objc", "objcpp"] \ } \}) < Note: this function can be called multiple times. Note: this function can be called before coc.nvim started. Note: this function can work alongside the user configuration file, but it's not recommended to use both. Note: use |g:coc_user_config| when you have coc.nvim lazy loaded. coc#add_command({id}, {command}, [{title}]) *coc#add_command()* Add custom Vim command to commands list opened by `:CocList commands` . Example: > call coc#add_command('mundoToggle', 'MundoToggle', \ 'toggle mundo window') < coc#expandable() *coc#expandable()* Check if a snippet is expandable at the current position. Requires `coc-snippets` extension installed. coc#jumpable() *coc#jumpable()* Check if a snippet is jumpable at the current position. coc#expandableOrJumpable() *coc#expandableOrJumpable()* Check if a snippet is expandable or jumpable at the current position. Requires `coc-snippets` extension installed. coc#on_enter() *coc#on_enter()* Notify coc.nvim that has been pressed. Used for the format on type and improvement of brackets insert, example: > " Confirm the completion when popupmenu is visible, insert and " notify coc.nvim otherwise. inoremap coc#pum#visible() ? coc#pum#confirm() \: "\u\\=coc#on_enter()\" < To enable format on type, use |coc-preferences-formatOnType| configuration. coc#status([{escape}]) *coc#status()* Return a status string that can be used in the status line, the status includes diagnostic information from |b:coc_diagnostic_info| and extension contributed statuses from |g:coc_status|. For statusline integration, see |coc-status|. Escape '%' to '%%' when {escape} is truth value. coc#util#api_version() *coc#util#api_version()* Get coc.nvim's vim API version number, start from `1`. coc#util#job_command() *coc#util#job_command()* Get the job command used for starting the coc service. coc#util#get_config_home() *coc#util#get_config_home()* Get the config directory that contains the user's coc-settings.json. coc#util#get_data_home() *coc#util#get_data_home()* Get data home directory, return |g:coc_data_home| when defined, else use $XDG_CONFIG_HOME/coc when $XDG_CONFIG_HOME exists, else fallback to `~/AppData/Local/coc` on windows and `~/.config/coc` on other systems. coc#util#extension_root() *coc#util#extension_root()* Return extensions root of coc.nvim. coc#util#root_patterns() *coc#util#root_patterns()* Get root patterns used for current document. Result could be something like: > {'global': ['.git', '.hg', '.projections.json'], 'buffer': [], 'server': v:null} < coc#util#get_config({key}) *coc#util#get_config()* Get configuration of current document (mostly defined in coc-settings.json) by {key}, example: > :echo coc#util#get_config('coc.preferences') coc#snippet#next() *coc#snippet#next()* Jump to next placeholder, does nothing when |coc#jumpable| is 0. coc#snippet#prev() *coc#snippet#prev()* Jump to previous placeholder, does nothing when |coc#jumpable| is 0. *coc#pum* coc#pum#visible() *coc#pum#visible()* Check if customized popupmenu is visible like |pumvisible()| does. Return 1 when popup menu is visible. coc#pum#has_item_selected() *coc#pum#has_item_selected* Check if there is a completion item selected in the popup menu. Return number `1` when completion item is selected. coc#pum#next({insert}) *coc#pum#next()* Select next item of customized popupmenu, insert word when {insert} is 1. Note: this function should only be used in key-mappings. coc#pum#prev({insert}) *coc#pum#prev()* Select previous item of customized popupmenu, insert word when {insert} is truth value. Note: this function should only be used in key-mappings. coc#pum#stop() *coc#pum#stop()* Close the customized popupmenu and stop the completion, works like of vim. Note: this function should only be used in key-mappings. coc#pum#cancel() *coc#pum#cancel()* Close the customized popupmenu and reset the trigger input before cursor when the trigger changed by pum navigation, like of vim. Note: this function should only be used in key-mappings. coc#pum#insert() *coc#pum#insert()* Insert word of current selected item and finish completion. Unlike |coc#pum#confirm()|, no text edit would be applied and snippet would not be expanded. Note: this function should only be used in key-mappings. coc#pum#confirm() *coc#pum#confirm()* Confirm completion of the selected item (when possible) by insert the `word` of completion item when not inserted, and close the customized popup menu, like of vim. Trigger the optional `onCompleteDone` handler of completion source after buffer text changed. Note: this function should only be used in key-mappings. coc#pum#select_confirm() *coc#pum#select_confirm()* Select the first completion item if no complete item is selected, then confirm the completion like |coc#pum#confirm()|. Note: this function should only be used in key-mappings. coc#pum#info() *coc#pum#info()* Return information of the customized popupmenu, should only be used when |coc#pum#visible()| is 1. Result contains: index Current select item index, 0 based. scrollbar Non-zero if a scrollbar is displayed. row Screen row count, 0 based. col Screen column count, 0 based. width Width of pum, including padding and border. height Height of pum, including padding and border. size Count of displayed complete items. inserted Is |v:true| when there is item inserted. reversed Is |v:true| when pum shown above cursor and enable |suggest.reversePumAboveCursor| coc#pum#select({index}, {insert}, {confirm}) *coc#pum#select()* Selects a complete item in the completion popupmenu, with optional {insert} and {confirm} action. Return empty string. Note: when {insert} enabled or cancelled by `index = -1`, the current buffer state could be not synchronized (to have better performance with dot repeat support |feedkeys()| with `\` is used to insert word when necessary). Use |timer_start()| afterwards to wait for the buffer text synchronize and `stopCompletion` request finish when needed. Parameters: ~ {index} Index (zero-based) of the item to select. When is `-1` the completion is canceled. Throw error when index out of range. {insert} Whether the word of selected completion item should be inserted to the buffer. {confirm} Confirm the completion and dismiss the popupmenu, implies `insert = 1`. coc#pum#one_more() *coc#pum#one_more()* Insert one more character from current complete item (first complete item when no complete item selected), works like of |popupmenu-keys|. Note that the word of complete item should starts with current input. Nothing happens when failed. Note: this function should only be used in key-mappings. coc#pum#scroll({forward}) *coc#pum#scroll()* Scroll the popupmenu forward or backward by page. Timer is used to make it works as {rhs} of key-mappings. Return . Parameters: ~ {forward} Scroll forward when none zero. coc#inline#trigger([{option}]) *coc#inline#trigger()* Parameters: ~ {option} Optional option object. Which can have following properties: - `provider` the provider name (the extension name or Language Client id which register inline completion provider), all valid providers will be requested in parallel when not specified. - `silent` when truthy no message would be shown when no inline completion items exists. Return `""` coc#inline#visible() *coc#inline#visible()* Return `1` when inline completion visual text exists for current buffer. coc#inline#cancel() *coc#inline#cancel()* Cancel the inline completion request and clear inline completion virtual text of current buffer. Return `""`. Note: this function uses |CocActionAsync|, use it in key-mappings instead of API. coc#inline#accept([{kind}]) *coc#inline#accept()* Accept current inline completion by insert text to current buffer when possible, nothing happens when inline completion is not activated. Parameters: ~ {kind} Optional accept kind of inline completion, could be one of "all", "word", "line". Default to "all" which means accept full insert text of complete item. Note: this function uses |CocActionAsync|, use it in key-mappings instead of API. coc#inline#next() *coc#inline#next()* Navigate to next inline complete item. Return `""`. Note: this function uses |CocActionAsync|, use it in key-mappings instead of API. coc#inline#prev() *coc#inline#prev()* Navigate to previous inline complete item. Return `""`. Note: this function uses |CocActionAsync|, use it in key-mappings instead of API. *coc#notify* coc#notify#close_all() *coc#notify#close_all()* Close all notification windows. coc#notify#do_action([{winid}]) *coc#notify#do_action()* Invoke action for all notification windows, or particular window with winid. coc#notify#copy() *coc#notify#copy()* Copy all content from notifications to system clipboard. coc#notify#show_sources() *coc#notify#show_sources()* Show source name (extension name) in notification windows. coc#notify#keep() *coc#notify#keep()* Stop auto hide timer of notification windows. coc#float#has_float([{all}]) *coc#float#has_float()* Check if float window/popup exists, check coc.nvim's float window/popup by default. coc#float#close_all([{all}]) *coc#float#close_all()* Close all float windows/popups created by coc.nvim, set {all} to `1` for all float window/popups. Return `""`. coc#float#close({winid}) *coc#float#close()* Close float window/popup with {winid}. coc#float#has_scroll() *coc#float#has_scroll()* Return `1` when there is scrollable float window/popup created by coc.nvim. Example key-mappings: > nnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\" nnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\" inoremap coc#float#has_scroll() ? "\=coc#float#scroll(1)\" : "\" inoremap coc#float#has_scroll() ? "\=coc#float#scroll(0)\" : "\" vnoremap coc#float#has_scroll() ? coc#float#scroll(1) : "\" vnoremap coc#float#has_scroll() ? coc#float#scroll(0) : "\" < coc#float#scroll({forward}, [{amount}]) *coc#float#scroll()* Scroll all scrollable float windows/popups, scroll backward when {forward} is not `1`. {amount} could be number or full page when omitted. Popup menu is excluded. coc#compat#call({name}, {args}) *coc#compat#call()* Call api function {name} with {args} list (starts with `nvim_`) on vim or neovim, use: > :echo coc#api#Get_api_info()[1]['functions'] < on vim9 to get supported apis on vim. CocRequest({id}, {method}, [{params}]) *CocRequest()* Send a request to language client of {id} with {method} and optional {params}. Example: > call CocRequest('tslint', 'textDocument/tslint/allFixes', \ {'textDocument': {'uri': 'file:///tmp'}}) < Vim error will be raised if the response contains an error. *CocRequestAsync()* CocRequestAsync({id}, {method}, [{params}, [{callback}]]) Send async request to remote language server. {callback} function is called with error and response. CocNotify({id}, {method}, [{params}]) *CocNotify()* Send notification to remote language server, example: > call CocNotify('ccls', '$ccls/reload') < *CocRegisterNotification()* CocRegisterNotification({id}, {method}, {callback}) Register notification callback for specified client {id} and {method}, example: > autocmd User CocNvimInit call CocRegisterNotification('ccls', \ '$ccls/publishSemanticHighlight', function('s:Handler')) < {callback} is called with single param as notification result. Note: when register notification with same {id} and {method}, only the later registered would work. *CocLocations()* CocLocations({id}, {method}, [{params}, {openCommand}]) Send location request to language client of {id} with {method} and optional {params}. e.g.: > call CocLocations('ccls', '$ccls/call', {'callee': v:true}) call CocLocations('ccls', '$ccls/call', {}, 'vsplit') < {openCommand}: optional command to open buffer, default to `coc.preferences.jumpCommand` , |:edit| by default. When it's `v:false` locations list would always used. *CocLocationsAsync()* CocLocationsAsync({id}, {method}, [{params}, {openCommand}]) Same as |CocLocations()|, but send notification to server instead of request. CocAction({action}, [...{args}]) *CocAction()* Run {action} of coc with optional extra {args}. Checkout |coc-actions| for available actions. Note: it's recommended to use |CocActionAsync()| unless you have to block your vim. *CocActionAsync()* CocActionAsync({action}, [...{args}, [{callback}]]) Call CocAction by send notification to NodeJS process of coc.nvim. When callback function exists as the last argument, the callback function is called with `error` string as the first argument and `result` as the second argument. When no callback exists, error message would be echoed. Checkout |coc-actions| for available actions. Note: callback function required to get the vim state after the action. CocHasProvider({feature}, [{bufnr}]) *CocHasProvider()* Check if provider exists for specified feature of current or {bufnr} buffer. Supported features: `rename` `onTypeEdit` `documentLink` `documentColor` `foldingRange` `format` `codeAction` `workspaceSymbols` `formatRange` `hover` `signature` `documentSymbol` `documentHighlight` `definition` `declaration` `typeDefinition` `reference` `implementation` `codeLens` `selectionRange` `formatOnType` `callHierarchy` `semanticTokens` `semanticTokensRange` `linkedEditing` `inlayHint` `inlineValue` `typeHierarchy` CocTagFunc({pattern}, {flags}, {info}) *CocTagFunc()* Used for vim's 'tagfunc' option, to make tag search by |CTRL-]| use coc.nvim as provider, tag search would be performed when no result from coc.nvim. Make sure your vim support 'tagfunc' by > :echo exists('&tagfunc') < -------------------------------------------------------------------------------- *coc-actions* Available Actions ~ Acceptable {action} names for |CocAction()| and |CocActionAsync()|. "addWorkspaceFolder" {folder} *CocAction('addWorkspaceFolder')* Add {folder} to workspace folders, {folder} should be exists directory on file system. "removeWorkspaceFolder" {folder} *CocAction('removeWorkspaceFolder')* Remove workspace fold {folder}, {folder} should be exists directory on file system. "ensureDocument" [{bufnr}] *CocAction('ensureDocument')* Ensure current or specified document is attached to coc.nvim |coc-document-attached|, should be used when you need invoke action of current document on buffer create. Return |v:false| when document can't be attached. "diagnosticList" *CocAction('diagnosticList')* Get all diagnostic items of the current Neovim session. "diagnosticInfo" [{target}] *CocAction('diagnosticInfo')* Show diagnostic message at the current position, do not truncate. Optional {target} could be `float` or `echo`. "diagnosticToggle" [{enable}] *CocAction('diagnosticToggle')* Enable/disable diagnostics on the fly. This setting is ignored if `displayByAle` is enabled. You can toggle by specifying {enable}. {enable} can be 0 or 1 "diagnosticToggleBuffer" [{bufnr}] [{enable}] *CocAction('diagnosticToggleBuffer')* Toggle diagnostics for specific buffer, current buffer is used when {bufnr} not provided. 0 for current buffer You can toggle by specifying {enable}. {enable} can be 0 or 1 Note: this will only affect diagnostics shown in the UI, list of all diagnostics won't change. "diagnosticPreview" *CocAction('diagnosticPreview')* Show diagnostics under current cursor in preview window. "diagnosticRefresh" [{bufnr}] *CocAction('diagnosticRefresh')* Force refresh diagnostics for special buffer with {bufnr} or all buffers when {bufnr} doesn't exist, returns `v:null` before diagnostics are shown. NOTE: Will refresh in any mode. Useful when `diagnostic.autoRefresh` is `false`. "sourceStat" *CocAction('sourceStat')* get the list of completion source stats for the current buffer. "toggleSource" {source} *CocAction('toggleSource')* enable/disable {source}. "definitions" *CocAction('definitions')* Get definition locations of symbol under cursor. Return LSP `Location[]` "declarations" *CocAction('declarations')* Get declaration location(s) of symbol under cursor. Return LSP `Location | Location[] | LocationLink[]` "implementations" *CocAction('implementations')* Get implementation locations of symbol under cursor. Return LSP `Location[]` "typeDefinitions" *CocAction('typeDefinitions')* Get type definition locations of symbol under cursor. Return LSP `Location[]` "references" [{excludeDeclaration}] *CocAction('references')* Get references location list of symbol under cursor. {excludeDeclaration}: exclude declaration locations when not zero. Return LSP `Location[]` "jumpDefinition" [{openCommand}] *CocAction('jumpDefinition')* jump to definition locations of the current symbol. Return `v:false` when location not found. |coc-list-location| is used when more than one position is available, for custom location list, use variable: |g:coc_enable_locationlist|. To always use |coc-list-location|| for locations, use `v:false` for {openCommand}. {openCommand}: optional command to open buffer, default to `coc.preferences.jumpCommand` in `coc-settings.json` "jumpDeclaration" [{openCommand}] *CocAction('jumpDeclaration')* jump to declaration locations of the current symbol. Return `v:false` when location not found. same behavior as "jumpDefinition". When {openCommand} is `v:false`, location list would be always used. "jumpImplementation" [{openCommand}] *CocAction('jumpImplementation')* Jump to implementation locations of the current symbol. Return `v:false` when location not found. same behavior as "jumpDefinition" "jumpTypeDefinition" [{openCommand}] *CocAction('jumpTypeDefinition')* Jump to type definition locations of the current symbol. Return `v:false` when location not found. same behavior as "jumpDefinition" "jumpReferences" [{openCommand}] *CocAction('jumpReferences')* Jump to references locations of the current symbol, use |CocAction('jumpUsed')| to exclude declaration locations. Return `v:false` when location not found. same behavior as "jumpDefinition" "jumpUsed" [{openCommand}] *CocAction('jumpUsed')* Jump references locations without declarations. same behavior as "jumpDefinition" "getHover" [{hoverLocation}] *CocAction('getHover')* Get documentation text array on {hoverLocation} or current position, returns array of string. {hoverLocation} could contains: • bufnr: optional buffer number. • line: 1 based line number. • col: 1 based col number Throw error when buffer with bufnr is not attached. "doHover" [{hoverTarget}] *CocAction('doHover')* Show documentation of current symbol, return `v:false` when hover not found. {hoverTarget}: optional specification for where to show hover info, defaults to `coc.preferences.hoverTarget` in `coc-settings.json`. Valid options: ["preview", "echo", "float"] "definitionHover" [{hoverTarget}] *CocAction('definitionHover')* Same as |CocAction('doHover')|, but includes definition contents from definition provider when possible. "showSignatureHelp" *CocAction('showSignatureHelp')* Echo signature help of current function, return `v:false` when signature not found. You may want to set up an autocmd like this: > "getCurrentFunctionSymbol" *CocAction('getCurrentFunctionSymbol')* Return the function string that current cursor in. "documentSymbols" [{bufnr}] *CocAction('documentSymbols')* Get a list of symbols of current buffer or specific {bufnr}. "rename" *CocAction('rename')* Rename the symbol under the cursor position, |coc-dialog-input| would be shown for prompt a new name. Show error message when the provider not found or prepare rename failed. The buffers are not saved after apply workspace edits, use |:wa| to save all buffers. It's possible to undo/redo and inspect the changes, see |coc-workspace-edits|. Note: coc.nvim supports rename for disk files, but your language server may not. "refactor" *CocAction('refactor')* Open |coc-refactor-buffer| with current symbol as activated cursor ranges. Requires LSP rename support enabled with current buffer, use |:CocSearch| when rename support is not available. "format" *CocAction('format')* Format current buffer using the language server. Return `v:false` when format failed. "formatSelected" [{mode}] *CocAction('formatSelected')* Format the selected range, {mode} should be one of visual mode: `v` , `V`, `char`, `line`. When {mode} is omitted, it should be called using |formatexpr|. "snippetCancel" *CocAction('snippetCancel')* Cancel current snippet session. "snippetInsert" {range} {snippet} [{mode}] *CocAction('snippetInsert')* Insert {snippet} text as specific {range} of current buffer. {range} should be valid LSP range like: > // all 0 based utf16 unit code index. {"start": {"line": 0, "character": 1}, "end": {"line": 0, "character": 3}} < {snippet} is the textmate format snippet text used by VSCode. {mode} could be 1 or 2, use 1 to disable format of snippet. "selectionRanges" *CocAction('selectionRanges')* Get selection ranges of current position from language server. "services" *CocAction('services')* Get an information list for all services. "toggleService" {serviceId} *CocAction('toggleService')* Start or stop a service. "codeAction" [{mode}] [{only}] [{include_disabled}] *CocAction('codeAction')* Prompt for a code action and do it. {mode} could be `currline` or `cursor` or result of |visualmode()|, current buffer range is used when it's empty string. {only} can be title of a codeAction or list of CodeActionKind. {include_disabled} include disabled actions when is truth value. "codeActionRange" {start} {end} [{kind}] *CocAction('codeActionRange')* Run code action for range. {start} Start line number of range. {end} End line number of range. {kind} Code action kind, see |CocAction('codeActions')| for available action kind. Can be used to create commands like: > command! -nargs=* -range CocAction :call CocActionAsync('codeActionRange', , , ) command! -nargs=* -range CocFix :call CocActionAsync('codeActionRange', , , 'quickfix') < "codeLensAction" *CocAction('codeLensAction')* Invoke the command for codeLens of current line (or the line that contains codeLens just above). Prompt would be shown when multiple actions are available. "commands" *CocAction('commands')* Get a list of available service commands for the current buffer. "runCommand" [{name}] [...{args}] *CocAction('runCommand')* Run a global command provided by the language server. If {name} is not provided, a prompt with a list of commands is shown to be selected. {args} are passed as arguments of command. You can bind your custom command like so: > command! -nargs=0 OrganizeImport \ :call CocActionAsync('runCommand', 'editor.action.organizeImport') < "fold" {{kind}} *CocAction('fold')* Fold the current buffer, optionally use {kind} for specific FoldingRangeKind. {kind} could be 'comment', 'imports' or 'region'. Return `v:false` when failed. You can create a custom command like: > command! -nargs=? Fold :call CocAction('fold', ) < "highlight" *CocAction('highlight')* Highlight the symbols under the cursor. "openLink" [{command}] *CocAction('openLink')* Open a link under the cursor with {command}. {command} default to `edit`. File and URL links are supported, return `v:false` when failed. URI under cursor would be searched when no link returned from the "documentLink" provider. Configure |g:coc_open_url_command| for custom command to open remote url. "links" *CocAction('links')* Return document link list of current buffer. "extensionStats" *CocAction('extensionStats')* Get all extension states as a list. Including `id`, `root` and `state`. State could be `disabled`, `activated` and `loaded`. "toggleExtension" {id} *CocAction('toggleExtension')* Enable/disable an extension. "uninstallExtension" {id} *CocAction('uninstallExtension')* Uninstall an extension. "reloadExtension" {id} *CocAction('reloadExtension')* Reload an activated extension. "activeExtension" {id} *CocAction('activeExtension')* Activate extension of {id}. "deactivateExtension" {id} *CocAction('deactivateExtension')* Deactivate extension of {id}. "pickColor" *CocAction('pickColor')* Change the color at the current cursor position, requires `documentColor` provider |CocHasProvider|. Note: only works on mac or when you have python support on Vim and have the GTK module installed. "colorPresentation" *CocAction('colorPresentation')* Change the color presentation at the current color position, requires `documentColor` provider |CocHasProvider|. "codeActions" [{mode}] [{only}] *CocAction('codeActions')* Get codeActions list of current document. {mode} can be result of |visualmode()| for visual selected range. When it's falsy value, current file is used as range. {only} can be array of codeActionKind, possible values including: - 'refactor': Base kind for refactoring actions - 'quickfix': base kind for quickfix actions - 'refactor.extract': Base kind for refactoring extraction actions - 'refactor.inline': Base kind for refactoring inline actions - 'refactor.rewrite': Base kind for refactoring rewrite actions - 'source': Base kind for source actions - 'source.organizeImports': Base kind for an organize imports source action - 'source.fixAll': Base kind for auto-fix source actions {only} can also be string, which means filter by title of codeAction. "organizeImport" *CocAction('organizeImport')* Run organize import code action for current buffer. Return `false` when the code action not exists. "fixAll" *CocAction('fixAll')* Run fixAll codeAction for current buffer. Show warning when codeAction not found. "quickfixes" [{visualmode}] *CocAction('quickfixes')* Get quickfix codeActions of current buffer. Add {visualmode} as second argument get quickfix actions with range of latest |visualmode()| "doCodeAction" {codeAction} *CocAction('doCodeAction')* Do a codeAction. "doQuickfix" *CocAction('doQuickfix')* Do the first preferred quickfix action on current line. Throw error when no quickfix action found. "addRanges" {ranges} *CocAction('addRanges')* Ranges must be provided as array of range type: https://git.io/fjiEG "getWordEdit" *CocAction('getWordEdit')* Get workspaceEdit of current word, language server used when possible, extract word from current buffer as fallback. "getWorkspaceSymbols" {input} *CocAction('getWorkspaceSymbols')* Get workspace symbols from {input}. "resolveWorkspaceSymbol" {symbol} *CocAction('resolveWorkspaceSymbol')* Resolve location for workspace {symbol}. "showOutline" [{keep}] *CocAction('showOutline')* Show |coc-outline| for current buffer. Does nothing when outline window already shown for current buffer. {keep} override `"outline.keepWindow"` configuration when specified. Could be 0 or 1. Returns after window is shown (document symbol request is still in progress). "hideOutline" *CocAction('hideOutline')* Close |coc-outline| on current tab. Throws vim error when it can't be closed by vim. "incomingCalls" [{CallHierarchyItem}] *CocAction('incomingCalls')* Retrieve incoming calls from {CallHierarchyItem} or current position when not provided. "outgoingCalls" [{CallHierarchyItem}] *CocAction('outgoingCalls')* Retrieve outgoing calls from {CallHierarchyItem} or current position when not provided. "showIncomingCalls" *CocAction('showIncomingCalls')* Show incoming calls of current function with |coc-tree|, see |coc-callHierarchy| "showOutgoingCalls" *CocAction('showOutgoingCalls')* Show outgoing calls of current function with |coc-tree|. "showSuperTypes" *CocAction('showSuperTypes')* Show super types of types under cursor with |coc-tree|, see |coc-typeHierarchy|. A warning is shown when no types found under cursor. "showSubTypes" *CocAction('showSubTypes')* Show sub types of types under cursor with |coc-tree|, see |coc-typeHierarchy|. A warning is shown when no types found under cursor. "semanticHighlight" *CocAction('semanticHighlight')* Request semantic tokens highlight for current buffer. "inspectSemanticToken" *CocAction('inspectSemanticToken')* Inspect semantic token information at cursor position. "rangeSelect" {visualmode} {forward} *CocAction('rangeSelect')* Visual select previous or next selection range, requires `selectionRange` provider. {visualmode} should be result of {visualmode} or "" for current cursor position. {forward} select backward when it's falsy value. "sendRequest" {id} {method} [{params}] *CocAction('sendRequest')* Send LSP request with {method} and {params} to the language server of {id} (check the id by `:CocList services`). Checkout the LSP specification at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ "sendNotification" {id} {method} [{params}] *CocAction('sendNotification')* Send LSP notification to language server with {id}. -------------------------------------------------------------------------------- COMMANDS *coc-commands* :CocStart *:CocStart* Start the coc.nvim server, do nothing if the server already started. :CocRestart *:CocRestart* Restart coc.nvim service. Use this command when you want coc to start all over again. :CocPrintErrors *:CocPrintErrors* Show errors from stderr of NodeJS process in a split window. :CocDisable *:CocDisable* Disable handling vim events, useful for debug performance issues. To disable dynamic autocmds registered by extensions, use: > :call coc#clearGroups('coc_dynamic_') < :CocEnable *:CocEnable* Enable handling vim events. :CocConfig *:CocConfig* Edit the user config file `coc-settings.json` in |coc#util#get_config_home()| :CocLocalConfig *:CocLocalConfig* Edit or create `.vim/coc-settings.json` in current workspace folder. :CocInstall [{option}] {name} ... *:CocInstall* Install one or more coc extensions. {option}: could be `-sync` for use blocked process to download instead of terminal. Examples: > " Install latest coc-omni :CocInstall coc-omni " Install coc-omni 1.0.0 :CocInstall coc-omni@1.0.0 " Install snippet extension from github :CocInstall https://github.com/dsznajder/vscode-es7-javascript-react-snippets < :CocUninstall {name} *:CocUninstall* Uninstall an extension, use to complete the extension name. Note: the data create by extension is not cleaned up, you may have to manually remove them. :CocUpdate *:CocUpdate* Update all coc extensions to the latest version. :CocUpdateSync *:CocUpdateSync* Block version of update coc extensions. :CocCommand [{name}] [{args}] ... *:CocCommand* Run a command provided by coc.nvim or contributed by extensions, use `` for name completion. Open |coc-list-commands| when {name} not provided. Note: This command send notification to coc.nvim, to perform task after the command use |CocAction('runCommand')| instead. :CocOpenLog *:CocOpenLog* Open log file of coc.nvim. Use environmental variable `NVIM_COC_LOG_FILE` for fixed log file. Note: the log would be cleared when coc.nvim started. Use environment variable `NVIM_COC_LOG_LEVEL` to change log level (default 'info', could be 'all', 'trace', 'debug', 'info', 'warn', 'error', 'off'). Use shell command: > export NVIM_COC_LOG_LEVEL=debug < or add: > let $NVIM_COC_LOG_LEVEL='debug' < to your `.vimrc` :CocInfo *:CocInfo* Show version and log information in a split window, useful for submitting a bug report. :CocDiagnostics [height] *:CocDiagnostics* Open vim's |location-list| with diagnostics of current buffer. The location list is automatically updated by default. When multiple location list are opened for one buffer, only first one would be automatically updated. :CocSearch *:CocSearch* Perform search by ripgrep https://github.com/BurntSushi/ripgrep, and open |coc-refactor-buffer| with searched results. Note: the search is performed on your files, so normally you should save your buffers before invoke this command. Common arguments for ripgrep: ~ `-e` `--regexp`: treat search pattern as regexp. `-F` `--fixed-strings`: treat search pattern as fixed string. `-L` `--follow`: follow symbolic links while traversing directories. `-g` `--glob` {GLOB}: Include or exclude files and directories for searching that match the given glob. `--hidden`: Search hidden files and directories. `--no-ignore-vcs`: Don't respect version control ignore files (.gitignore, etc.). `--no-ignore`: Don't respect ignore files (.gitignore, .ignore, etc.). `-w` `--word-regexp`: Only show matches surrounded by word boundaries. `-S` `--smart-case`: Searches case insensitively if the pattern is all lowercase. Search case sensitively otherwise. `--no-config`: Never read configuration files. `-x` `--line-regexp`: Only show matches surrounded by line boundaries. Use `:man 1 rg` in your terminal for more details. Note: By default, hidden files and directories are skipped. Note: By default, vcs ignore files including `.gitignore` and `.ignore` are respected Escape arguments: ~ || is used to convert command line arguments to arguments of rg, which means you have to escape space for single argument. For example, if you want to search `import { Neovim` , you have to use: > :CocSearch import\ \{\ Neovim < The escape for `{` is required because rg use regexp be default, or: > :CocSearch -F import\ {\ Neovim < for strict match. Change and save: ~ Refactor session is started with searched patterns highlighted, just change the text and save refactor buffer to make changes across all related files. You can make any kind of changes, including add lines and remove lines. :CocWatch [extension] *:CocWatch* Watch loaded [extension] for reload on file change, use for complete extension id. :CocOutline *:CocOutline* Invoke |CocAction('showOutline')| by notification. -------------------------------------------------------------------------------- AUTOCMD *coc-autocmds* *CocLocationsChange* :autocmd User CocLocationsChange {command} For building a custom view of locations, set |g:coc_enable_locationlist| to 0 and use this autocmd with with |g:coc_jump_locations| For example, to disable auto preview of location list, use: > let g:coc_enable_locationlist = 0 autocmd User CocLocationsChange CocList --normal location < *CocNvimInit* :autocmd User CocNvimInit {command} Triggered after the coc services have started. If you want to trigger an action of coc after Vim has started, this autocmd should be used because coc is always started asynchronously. *CocStatusChange* :autocmd User CocStatusChange {command} Triggered after |g:coc_status| changed, can be used for refresh statusline. *CocDiagnosticChange* :autocmd User CocDiagnosticChange {command} Triggered after the diagnostic status has changed. Could be used for updating the statusline. *CocJumpPlaceholder* :autocmd User CocJumpPlaceholder {command} Triggered after cursor jump to a placeholder. *CocOpenFloat* :autocmd User CocOpenFloat {command} Triggered when a floating window is opened. The window is not focused, use |g:coc_last_float_win| to get window id. *CocOpenFloatPrompt* :autocmd User CocOpenFloatPrompt {command} Triggered when a floating prompt window is opened (triggered after |CocOpenFloat|). *CocTerminalOpen* :autocmd User CocTerminalOpen {command} Triggered when the terminal is shown, can be used for adjusting the window height. -------------------------------------------------------------------------------- HIGHLIGHTS *coc-highlights* The best place to override the highlights is with a |ColorScheme| autocommand: > " make error texts have a red color autocmd ColorScheme solarized \ highlight CocErrorHighlight ctermfg=Red guifg=#ff0000 < Use |:highlight| with group name to check current highlight. Note: don't use `:hi default` for overwriting the highlights. Note: user defined highlight commands should appear after the |:colorscheme| command and use |ColorScheme| autocmd to make sure customized highlights works after color scheme change. Markdown related ~ *CocBold* for bold text. *CocItalic* for italic text. *CocUnderline* for underlined text. *CocStrikeThrough* for strikethrough text, like usage of deprecated API. *CocMarkdownCode* for inline code in markdown content. *CocMarkdownHeader* for markdown header in floating window/popup. *CocMarkdownLink* for markdown link text in floating window/popup. Diagnostics related ~ *coc-highlights-diagnostics* *CocFadeOut* for faded out text, such as for highlighting unnecessary code. *CocErrorSign* for error signs. *CocWarningSign* for warning signs. *CocInfoSign* for information signs. *CocHintSign* for hint signs. *CocErrorVirtualText* for error virtual text. *CocWarningVirtualText* for warning virtual text. *CocInfoVirtualText* for information virtual text. *CocHintVirtualText* for hint virtual text. *CocErrorHighlight* for error code range. *CocWarningHighlight* for warning code range. *CocInfoHighlight* for information code range. *CocHintHighlight* for hint code range. *CocDeprecatedHighlight* for deprecated code range, links to |CocStrikeThrough| by default. *CocUnusedHighlight* for unnecessary code range, links to |CocFadeOut| by default. *CocErrorLine* line highlight of sign which contains error. *CocWarningLine* line highlight of sign which contains warning. *CocInfoLine* line highlight of sign which information. *CocHintLine* line highlight of sign which contains hint. Highlight with higher priority would overwrite highlight with lower priority. The priority order: |CocUnusedHighlight| > |CocDeprecatedHighlight| > |CocErrorHighlight| > |CocWarningHighlight| > |CocInfoHighlight| > |CocHintHighlight| Document highlight related ~ *coc-highlights-document* Highlights used for highlighting same symbols in the buffer at the current cursor position. *CocHighlightText* default symbol highlight. *CocHighlightRead* for `Read` kind of document symbol. *CocHighlightWrite* for `Write` kind of document symbol. Float window/popup related ~ *coc-highlights-float* *CocFloating* default highlight group of floating windows/popups. Default links to |NormalFloat| on neovim and |Pmenu| on vim. *CocFloatBorder* default border highlight group of floating windows/popups. Default links to |FloatBorder| when exists and |CocFloating| when not. Note: only foreground color is used for border highlight. *CocFloatThumb* thumb highlight of scrollbar. *CocFloatSbar* Scrollbar highlight of floating window/popups. *CocFloatDividingLine* for dividing lines, links to |NonText| by default. *CocFloatActive* for activated text, links to |CocSearch| by default. *CocErrorFloat* for error text in floating windows/popups. *CocHintFloat* for hint text in floating windows/popups. Inlay hint related ~ *coc-highlights-inlayHint* *CocInlayHint* for highlight inlay hint virtual text block, default uses foreground from |CocHintSign| and background from |SignColumn| *CocInlayHintParameter* for parameter kind of inlay hint. *CocInlayHintType* for type kind of inlay hint. Notification window/popup related ~ CocNotification *CocNotification* *CocNotificationProgress* for progress line in progress notification. *CocNotificationButton* for action buttons in notification window. *CocNotificationKey* for function keys which trigger actions in notification popups (vim9 only). *CocNotificationError* for highlight border of error notification. *CocNotificationWarning* for highlight border of warning notification. *CocNotificationInfo* for highlight border of info notification. List related ~ *CocList* *CocSearch* for matched characters. *CocListLine* for current cursor line in list window and preview window. *CocListSearch* for matched characters. *CocListMode* for mode text in the statusline. *CocListPath* for cwd text in the statusline. *CocSelectedText* for sign text of selected lines (multiple selection only). *CocSelectedLine* for line highlight of selected lines (multiple selection only). Tree view related ~ CocTree *CocTree* *CocTreeTitle* for title in tree view. *CocTreeDescription* for description beside label. *CocTreeOpenClose* for open and close icon in tree view. *CocTreeSelected* for highlight lines contains selected node. Popup menu related ~ *CocPum* *CocPumSearch* for matched input characters, linked to |CocSearch| by default. *CocPumDetail* for highlight label details that follows label (including possible detail and description). *CocPumMenu* for menu of complete item. *CocPumShortcut* for shortcut text of source. *CocPumDeprecated* for deprecated label. *CocPumVirtualText* for inserted virtual text from word of selected complete item, enabled by |coc-config-suggest-virtualText|. *CocInline* Inline completion related ~ *CocInlineVirtualText* for virtual text of |coc-inlineCompletion|, defaulting to a medium gray. *CocInlineAnnotation* for annotation virtual text of |coc-inlineCompletion|, defaulting to "MoreMsg". Symbol icons ~ CocSymbol *CocSymbol* Highlight groups for symbol icons, including `CompletionItemKind` and `SymbolKind` of LSP. The highlight groups link to related |nvim-treesitter| highlight groups when possible and fallback to builtin highlight groups. *CocSymbolDefault* linked to |hl-MoreMsg| by default. *CocSymbolText* *CocSymbolUnit* *CocSymbolValue* *CocSymbolKeyword* *CocSymbolSnippet* *CocSymbolColor* *CocSymbolReference* *CocSymbolFolder* *CocSymbolFile* *CocSymbolModule* *CocSymbolNamespace* *CocSymbolPackage* *CocSymbolClass* *CocSymbolMethod* *CocSymbolProperty* *CocSymbolField* *CocSymbolConstructor* *CocSymbolEnum* *CocSymbolInterface* *CocSymbolFunction* *CocSymbolVariable* *CocSymbolConstant* *CocSymbolString* *CocSymbolNumber* *CocSymbolBoolean* *CocSymbolArray* *CocSymbolObject* *CocSymbolKey* *CocSymbolNull* *CocSymbolEnumMember* *CocSymbolStruct* *CocSymbolEvent* *CocSymbolOperator* *CocSymbolTypeParameter* Note: Use configuration |coc-config-suggest-completionItemKindLabels| for customized icon characters. Semantic token highlight groups ~ *CocSem* Semantic highlight groups are starts with `CocSem` which link to related |nvim-treesitter| highlight groups when possible and fallback to builtin highlight groups, use variable |g:coc_default_semantic_highlight_groups| to disable creation of these highlight groups. The highlight groups rules: > `CocSemType + type` for types `CocSemTypeMod + type + modifier` for modifier with type < Only semantic tokens types and `deprecated` modifier have default highlight groups. You need create highlight groups for highlight other modifiers and/or specific modifier with type, for example: > " Add highlights for declaration modifier hi link CocSemTypeModClassDeclaration ClassDeclaration < The modifier highlight groups have higher priority. Others ~ *CocDisabled* highlight for disabled items, eg: menu item. *CocCodeLens* for virtual text of codeLens. *CocCursorRange* for highlight of activated cursors ranges. *CocLinkedEditing* for highlight of activated linked editing ranges. *CocHoverRange* for range of current hovered symbol. *CocMenuSel* for current menu item in menu dialog (should only provide background color). *CocSelectedRange* for highlight ranges of outgoing calls. *CocSnippetVisual* for highlight snippet placeholders. *CocLink* for highlight document links. *CocInputBoxVirtualText* for highlight placeholder of input box. ============================================================================== TREE SUPPORT *coc-tree* Tree view is used for render outline and call hierarchy, following features are supported: • Data update while keep tree node open/close state. • Auto refresh on load error. • Click open/close icon to toggle collapse state. • Click node to invoke default command. • Show tooltip in float window on |CursorHold| when possible. • Key-mappings support |coc-tree-mappings| • Optional multiple selection. • Optional node reveal support. • Optional fuzzy filter support. • Provide API `window.createTreeView` for extensions. Check |coc-config-tree| for related configurations. The filetype is `'coctree'`, which can be used to overwrite buffer and window options. Use variable |w:cocViewId| to detect the kind of tree. -------------------------------------------------------------------------------- TREE KEY MAPPINGS *coc-tree-mappings* Default key-mappings are provided for 'coctree' buffer, which can be changed by configuration |coc-config-tree|. - Select/unselect item, configured by `"tree.key.toggleSelection"`. - Invoke actions of current item, configured by `"tree.key.actions"`. - Close tree window, configured by `"tree.key.close"`. - Invoke command of current item, configured by `"tree.key.invoke"`. - Move cursor to original window. f - Activate filter, configured by `"tree.key.activeFilter"`. t - Trigger key to toggle expand state of tree node, configured by `tree.key.toggle`. M - Collapse all tree node, configured by `"tree.key.collapseAll"`. -------------------------------------------------------------------------------- TREE FILTER *coc-tree-filter* Filter mode is used for search for specific node by fuzzy filter, invoke the key configured by `"tree.key.activeFilter"` to activate filter mode. Note: some tree views not have filter mode supported. When filter mode is activated, type normal character to insert filter input and following special keys are supported: - Delete last filter character when possible. - Delete last filter character when possible. - Clean up filter text. - Navigate to previous filter text (stored on command invoke). - Navigate to next filter text (stored on command invoke). - exit filter mode. - exit filter mode. or `"tree.key.selectPrevious"` - Select previous node. or `"tree.key.selectNext"` - Select next node. or `"key.key.invoke"` - Invoke command of selected node. ============================================================================== LIST SUPPORT *coc-list* Built-in list support to make working with lists of items easier. The following features are supported: • Insert & normal mode. • Default key-mappings for insert & normal mode. • Customize key-mappings for insert & normal mode. • Commands for reopening & doing actions with a previous list. • Different match modes. • Interactive mode. • Auto preview on cursor move. • Number select support. • Built-in actions for locations. • Parse ANSI code. • Mouse support. • Select actions using . • Multiple selections using in normal mode. • Select lines by visual selection. To enable set filetype of preview window, use |g:coc_list_preview_filetype|. -------------------------------------------------------------------------------- LIST COMMAND *coc-list-command* :CocList [{...options}] [{source}] [{...args}] *:CocList* Open coc list of {source}, example: > :CocList --normal location < For current jump locations. For {options}, see |coc-list-options|. Also check |coc-config-list| for list configuration. {args} are sent to source during the fetching of list. Press `?` on normal mode to get supported {args} of current list. When {source} is empty, the lists source with list of sources is used. :CocListResume [{name}] *:CocListResume* Reopen last opened list, input and cursor position will be preserved. :CocListCancel *:CocListCancel* Close list, useful when the list is not the current window. :CocPrev [{name}] *:CocPrev* Invoke default action for the previous item in the last {name} list. Doesn't open the list window if it's closed. :CocNext [{name}] *:CocNext* Invoke the default action for the next item in the last {name} list. Doesn't open the list window if it's closed. :CocFirst [{name}] *:CocFirst* Invoke default action for first item in the last {name} list. :CocLast [{name}] *:CocLast* Invoke default action for last item in the last {name} list. *coc-list-options* Options of CocList command ~ --top Show list as top window. --tab Open list in new tabpage. --normal Start list in normal mode, recommended for short list. --no-sort Disable sort made by fuzzy score or most recently used, use it when it's already sorted. --input={input} Specify the input on session start. --height={number} Specify the height of list window, override configuration |coc-config-list-height|. No effect when list opened in new tab by `--tab`. --strict, -S Use strict matching instead of fuzzy matching. --regex, -R Use regex matching instead of fuzzy matching. --ignore-case Ignore case when using strict matching or regex matching. --number-select, -N Type a line number to select an item and invoke the default action on insert mode. Type `0` to select the 10th line. --interactive, -I Use interactive mode, list items would be reloaded on input change, filter and sort would be done by list implementation. Note: only works when the list support interactive mode. Note: filtering and sorting would be done by underlying task, which means options including `--strict`, `--no-sort`, `--regex`, `--ignore-case` would not work at all. --auto-preview, -A Start a preview for the current item on the visible list. --no-quit Not quit list session after invoke action. Note: you may need to refresh the list for current state. --first Invoke default action for first list item on list open. Nothing happens when the list is empty. --reverse Reverse the order of list items shown in the window, the bottom line would shown the first item. -------------------------------------------------------------------------------- LIST CONFIGURATION *coc-list-configuration* Use `coc-settings.json` for configuration of lists. Configuration of list starts with 'list.'. See |coc-config-list| or type `list.` in your settings file to get completion list (requires coc-json installed). For configuration of a specified list, use section that starts with: `list.source.{name}`, where `{name}` is the name of list. Change default action: ~ If you want to use `tabe` as default action of symbols list, you can use: > // change default action of symbols "list.source.symbols.defaultAction": "tabe" < in your coc-settings.json Change default options: ~ To change |coc-list-options| for source with {name}, use `list.source.{name}.defaultOptions` configuration like: > // make symbols list use normal mode and interactive by default "list.source.symbols.defaultOptions": ["--interactive", "--number-select"], < Note: some list like symbols only work in interactive mode, you must include `--interactive` in `defaultOptions`. Note: default options will not be used when there're options passed with |:CocList| command. Change default arguments: ~ Use `list.source.{name}.defaultArgs` setting like: > // use regex match for grep source "list.source.grep.defaultArgs": ["-regex"], Note: default arguments used only when arguments from |:CocList| command is empty. Note: Type `?` on normal mode to get supported arguments of current list. -------------------------------------------------------------------------------- LIST MAPPINGS *coc-list-mappings* Default mappings on insert mode: - Cancel list session. - Do default action with selected items or current item. - Stop loading task. - Paste text from system clipboard. - Reload list. - Change to normal mode. - Select next line. - Select previous line. - Move cursor left. - Move cursor right. - Move cursor to end of prompt. - Same as . - Move cursor to start of prompt. - Same as . - Scroll window forward. - Scroll window backward. - Remove previous character of cursor. - Remove previous character of cursor. - Remove previous word of cursor. - Remove characters before cursor. - Navigate to next input in history. - Navigate to previous input in history. - Switch matcher for filter items. - Insert content from vim's register. - Select action. Default mappings on normal mode: - Cancel list session. - Do default action with selected items or current item. - Stop source from fetching more items. - Reload list. - Mark all visible items selected. - Jump to original window on list create. - Select action. - Scroll preview window down. - Scroll preview window up. - Toggle selection of current item. i,I,o,O,a,A - Change to insert mode. p - Preview action. : - Cancel the prompt and enter command mode. ? - Show help of current list. t - Do 'tabe' action. d - Do 'drop' action. s - Do 'split' action. r - Do 'refactor' action. Use |coc-list-mappings-custom| to override default mappings. *coc-list-mappings-custom* Configurations `"list.normalMappings"` and `"list.insertMappings"` are used for customizing the list key-mappings, example: > "list.insertMappings": { "": "do:previewtoggle", "": "do:help" "": "do:refresh", "": "feedkeys:\\", "": "feedkeys:\\", "": "normal:j", "": "normal:k", "": "action:tabe", "": "call:MyFunc", // paste yanked text to prompt "": "eval:@@" } "list.normalMappings": { "c": "expr:MyExprFunc" "d": "action:delete" } < Note: you should only use mappings that start with ` can't be remapped for other actions. The mapping expression should be `command:arguments`, available commands: 'do' - special actions provided by coc list, including: 'refresh' - reload list. 'selectall' - mark all visible items selected. 'switch' - switch matcher used for filter items. 'exit' - exit list session. 'stop' - stop loading task. 'cancel' - cancel list session but leave list window open. 'toggle' - toggle selection of current item. 'togglemode' - toggle between insert and normal mode. 'previous' - move cursor to previous item. 'next' - move cursor to next item. 'defaultaction' - do default action for selected item(s). 'chooseaction' - choose action for selected item(s). 'jumpback' - stop prompt and jump back to original window. 'previewtoggle' - toggle preview window, requires preview action exists. 'previewup' - scroll preview window up. 'previewdown' - scroll preview window down. 'help' - show help. 'prompt' - do prompt action, including: 'previous' - change to previous input in history. 'next' - change to next input in history. 'start' - move cursor to start. 'end' - move cursor to end. 'left' - move cursor left. 'right' - move cursor right. 'leftword' - move cursor left by a word. 'rightword' - move cursor right by a word. 'deleteforward' - remove previous character. 'deletebackward' - remove next character. 'removetail' - remove characters afterwards. 'removeahead' - remove character ahead. 'removeword' - remove word before cursor. 'insertregister' - insert content from Vim register. 'paste' - append text from system clipboard to prompt. 'eval' - append text to prompt from result of VimL expression. 'action' - execute action of list, use to find available actions. 'feedkeys' - feedkeys to list window, use `\\` in JSON to escape special characters. 'feedkeys!' - feedkeys without remap. 'normal' - execute normal command in list window. 'normal!' - execute normal command without remap. 'command' - execute command. 'call' - call Vim function with |coc-list-context| as only argument. 'expr' - same as 'call' but expect the function return action name. *coc-list-context* Context argument contains the following properties: 'name' - name of the list, example: `'location'`. 'args' - arguments of the list. 'input' - current input of prompt. 'winid' - window id on list activated. 'bufnr' - buffer number on list activated. 'targets' - list of selected targets, checkout |coc-list-target| for properties. *coc-list-target* Target contains the following properties: 'label' - mandatory property that is shown in the buffer. 'filtertext' - optional filter text used for filtering items. 'location' - optional location of item, check out https://bit.ly/2Rtb6Bo 'data' - optional additional properties. -------------------------------------------------------------------------------- LIST SOURCES *coc-list-sources* -------------------------------------------------------------------------------- location *coc-list-location* Last jump locations. Actions: - 'preview' : preview location in preview window. - 'open': open location by use `"coc.preferences.jumpCommand"`, default action - 'tabe': Use |:tabe| to open location. - 'drop': Use |:drop| to open location. - 'vsplit': Use |:vsplit| to open location. - 'split': Use |:split| to open location. - 'quickfix': Add selected items to Vim's quickfix. To customize filepath displayed in the list, user could inject javascript global function `formatFilepath` which accept filepath and should return string. ex: > const path = require('path') global.formatFilepath = function (file) { return file.startsWith('jdt:/') ? path.basename(file) : file } < save the file to `~/custom.js` and make coc load it by add > let g:coc_node_args = ['-r', expand('~/custom.js')] < to your vimrc. `formatFilepath` works for |coc-list-symbols| as well. extensions *coc-list-extensions* Manage coc.nvim extensions. First column in the list window represent the state of extension: - "*" means the extension is activated. - "+" means the extension package json is loaded, but not activated by load the javascript file. - "-" means the extension is disabled by 'disable' action. - "?" means the extension is not recognized by coc.nvim. Actions: - 'toggle' activate/deactivate extension, default action. - 'disable' disable extension. - 'enable' enable extension. - 'lock' lock/unlock extension to current version. - 'doc' view extension's README doc. - 'fix' fix dependencies in terminal buffer. - 'reload' reload extension. - 'uninstall' uninstall extension. diagnostics *coc-list-diagnostics* All diagnostics for the workspace. Actions: - Same as |coc-list-location| folders *coc-list-folders* Manage current workspace folders of coc.nvim. Actions: - 'edit' change the directory of workspace folder. - 'delete' remove selected workspace folder. outline *coc-list-outline* Symbols in the current document. Actions: - Same as |coc-list-location| symbols *coc-list-symbols* Search workspace symbols. Actions: - Same as |coc-list-location| services *coc-list-services* Manage registered services. Actions: - 'toggle': toggle service state, default action. commands *coc-list-commands* Workspace commands. Actions: - 'run': run selected command, default action. Builtin commands: - document.checkBuffer - document.echoFiletype - document.jumpToNextSymbol - document.jumpToPrevSymbol - document.renameCurrentWord - document.showIncomingCalls - document.showOutgoingCalls - document.toggleCodeLens - document.toggleColors - document.toggleInlayHint - editor.action.colorPresentation - editor.action.formatDocument - editor.action.organizeImport - editor.action.pickColor - extensions.forceUpdateAll - extensions.toggleAutoUpdate - semanticTokens.checkCurrent - semanticTokens.clearAll - semanticTokens.clearCurrent - semanticTokens.inspect - semanticTokens.refreshCurrent - workspace.clearWatchman - workspace.diagnosticRelated - workspace.inspectEdit - workspace.undo - workspace.redo - workspace.renameCurrentFile - workspace.showOutput - workspace.workspaceFolders links *coc-list-links* Links in the current document. Actions: - 'open': open the link, default action. - 'jump': jump to link definition. sources *coc-list-completion-sources* Available completion sources. Actions: - 'toggle': activate/deactivate source, default action. - 'refresh': refresh source. - 'open': open the file where source defined. lists *coc-list-lists* Get available lists. Actions: - 'open': open selected list, default action. ============================================================================== DIALOG SUPPORT *coc-dialog* Dialog is special float window/popup that could response to user actions, dialog have close button, border, title (optional), bottom buttons(optional). Note bottom buttons work different on neovim and vim, on neovim you can click the button since neovim allows focus of window, on vim you have to type highlighted character to trigger button callback. See |coc-config-dialog| for available configurations. -------------------------------------------------------------------------------- *coc-dialog-basic* A basic dialog is create by Javascript API `window.showDialog` , which is just some texts with optional buttons. -------------------------------------------------------------------------------- *coc-dialog-confirm* A confirm dialog is used for user to confirm an action, normally created by `window.showPrompt()` Confirm dialog uses filter feature on vim8 and |getchar()| on Neovim. The difference is you can operate vim on vim8, but not on neovim. Supported key-mappings: - force cancel, return -1 for callback. , n, N - reject the action, return 0 for callback. y,Y - accept the action, return 1 for callback. -------------------------------------------------------------------------------- *coc-dialog-input* An input dialog request user input with optional default value, normally created by `window.requestInput`, when `"coc.preferences.promptInput"` is false, vim's command line input prompt is used instead. On neovim, it uses float window, on vim8, it opens terminal in popup. Supported key-mappings: - move cursor to first col. - move cursor to last col. - cancel input, null is received by callback. - accept current input selection of current item. QuickPick related (available when created by |coc-dialog-quickpick|). - scroll forward quickpick list. - scroll backward quickpick list. - move to next item in quickpick list. - move to previous item in quickpick list. - toggle selection of current item in quickpick list when canSelectMany is supported. Note on neovim, other insert mode key-mappings could work. Note not possible to configure key-mappings on vim8, to customize key-mappings on neovim, use |CocOpenFloatPrompt| with current buffer. -------------------------------------------------------------------------------- *coc-dialog-quickpick* A quickpick is a input dialog in the middle with a float window/popup contains filtered list items. Fuzzy filter is used by default. See |coc-config-dialog| for available configurations. See |coc-dialog-input| for available key-mappings. -------------------------------------------------------------------------------- *coc-dialog-menu* A menu dialog is used for pick a single item from list of items, extensions could use `window.showMenuPicker` to create menu dialog. Supported key-mappings: - cancel selection. - confirm selection of current item, use |dialog.confirmKey| to override. 1-9 - select item with 1 based index. g - move to first item. G - move to last item. j - move to next item. k - move to previous item. - scroll forward. - scroll backward. -------------------------------------------------------------------------------- *coc-dialog-picker* A picker dialog is used for single/multiple selection. On neovim, it's possible to toggle selection by mouse click inside the bracket. Extensions could use `window.showPickerDialog` to create picker dialog. Supported key-mappings: - cancel selection. - confirm selection of current item, use |dialog.confirmKey| to override. - toggle selection of current item. g - move to first item. G - move to last item. j - move to next item. k - move to previous item. - scroll forward. - scroll backward. When close button is clicked, the selection is canceled with undefined result (same as ). It's recommended to use |coc-dialog-quickpick| for filter support. ============================================================================== NOTIFICATION SUPPORT *coc-notification* Notification windows are created at the bottom right of the screen. Notifications are created by Javascript APIs: `window.showErrorMessage()`, `window.showWarningMessage()`, `window.showInformationMessage()`, `window.showNotification()` and `window.withProgress()`. Possible kind of notifications: 'error', 'warning', 'info' and 'progress'. Message notifications (not progress) requires |coc-preferences-enableMessageDialog| to be `true`. Message notifications without actions would be automatically closed after milliseconds specified by |coc-config-notification-timeout|. Use |coc-config-notification-disabledProgressSources| to disable progress notifications for specific sources. Customize notifications: ~ • Customize icons: |g:coc_notify| • Customize highlights: |CocNotification| • Customize configurations: |coc-config-notification| Related functions: ~ • |coc#notify#close_all()| • |coc#notify#do_action()| • |coc#notify#copy()| • |coc#notify#show_sources()| • |coc#notify#keep()| ============================================================================== STATUSLINE SUPPORT *coc-status* Diagnostics info and other status info contributed by extensions could be shown in statusline. The easiest way is add `%{coc#status()}` to your 'statusline' option. Example: > set statusline^=%{coc#status()} < Use |CocStatusChange| autocmd for automatically refresh statusline: > autocmd User CocStatusChange redrawstatus < -------------------------------------------------------------------------------- *coc-status-manual* Create function like: > function! StatusDiagnostic() abort let info = get(b:, 'coc_diagnostic_info', {}) if empty(info) | return '' | endif let msgs = [] if get(info, 'error', 0) call add(msgs, 'E' . info['error']) endif if get(info, 'warning', 0) call add(msgs, 'W' . info['warning']) endif return join(msgs, ' ') . ' ' . get(g:, 'coc_status', '') endfunction < Add `%{StatusDiagnostic()}` to your 'statusline' option. -------------------------------------------------------------------------------- *coc-status-airline* With vim-airline: https://github.com/vim-airline/vim-airline See |airline-coc| ------------------------------------------------------------------------------ *coc-status-lightline* With lightline.vim: https://github.com/itchyny/lightline.vim Use configuration like: > let g:lightline = { \ 'colorscheme': 'wombat', \ 'active': { \ 'left': [ [ 'mode', 'paste' ], \ [ 'cocstatus', 'readonly', 'filename', 'modified' ] ] \ }, \ 'component_function': { \ 'cocstatus': 'coc#status' \ }, \ } " Use autocmd to force lightline update. autocmd User CocStatusChange,CocDiagnosticChange call lightline#update() < ============================================================================== CREATE PLUGINS *coc-plugins* There're different ways to extend coc.nvim: • Create vim completion sources |coc-api-vim-source|. • Create extensions |coc-api-extension|. • Create single file extensions |coc-api-single-file|. • Debug coc.nvim extension |coc-api-debug|. ============================================================================== FAQ *coc-faq* ------------------------------------------------------------------------------ Check out https://github.com/neoclide/coc.nvim/wiki/F.A.Q ============================================================================== CHANGELOG *coc-changelog* See ./history.md under project root. ============================================================================== vim:tw=78:nosta:noet:ts=8:sts=0:ft=help:noet:fen: ================================================ FILE: esbuild.js ================================================ const cp = require('child_process') let revision = 'master' if (process.env.NODE_ENV !== 'development') { try { let res = cp.execSync(`git log -1 --date=iso --pretty=format:'"%h","%ad"'`, {encoding: 'utf8'}) revision = res.replaceAll('"', '').replace(',', ' ') } catch (e) { // ignore } } let entryPlugin = { name: 'entry', setup(build) { build.onResolve({filter: /^index\.js$/}, args => { return { path: args.path, namespace: 'entry-ns' } }) build.onLoad({filter: /.*/, namespace: 'entry-ns'}, () => { let contents = `'use strict' if (global.__isMain) { Object.defineProperty(console, 'log', { value() { if (logger) logger.info(...arguments) } }) const { createLogger } = require('./src/logger/index') const logger = createLogger('server') process.on('uncaughtException', function(err) { let msg = 'Uncaught exception: ' + err.message console.error(msg) logger.error('uncaughtException', err.stack) }) process.on('unhandledRejection', function(reason, p) { if (reason instanceof Error) { if (typeof reason.code === 'number') { let msg = 'Unhandled response error ' + reason.code + ' from language server: ' + reason.message if (reason.data != null) { console.error(msg, reason.data) } else { console.error(msg) } } else { console.error('UnhandledRejection: ' + reason.message + '\\n' + reason.stack) } } else { console.error('UnhandledRejection: ' + reason) } logger.error('unhandledRejection ', p, reason) }) const attach = require('./src/attach').default attach({ reader: process.stdin, writer: process.stdout }) } else { const exports = require('./src/index') const logger = require('./src/logger').logger const attach = require('./src/attach').default module.exports = {attach, exports, logger, loadExtension: (filepath, active) => { return exports.extensions.manager.load(filepath, active) }} }` return { contents, resolveDir: __dirname } }) } } async function start() { await require('esbuild').build({ entryPoints: ['index.js'], bundle: true, sourcemap: process.env.NODE_ENV === 'development', define: { REVISION: '"' + revision + '"', ESBUILD: 'true', 'process.env.COC_NVIM': '"1"', 'global.__TEST__': 'false' }, mainFields: ['module', 'main'], platform: 'node', treeShaking: true, target: 'node16.18', plugins: [entryPlugin], banner: { js: `"use strict"; global.__starttime = Date.now(); global.__isMain = require.main === module;` }, outfile: 'build/index.js' }) } start().catch(e => { console.error(e) }) ================================================ FILE: eslint.config.mjs ================================================ import {defineConfig, globalIgnores} from "eslint/config" import jsdoc from "eslint-plugin-jsdoc" import jest from "eslint-plugin-jest" import typescriptEslint from "@typescript-eslint/eslint-plugin" import globals from "globals" import tsParser from "@typescript-eslint/parser" import path from "node:path" import {fileURLToPath} from "node:url" import js from "@eslint/js" import {FlatCompat} from "@eslint/eslintrc" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all }) export default defineConfig([ globalIgnores(["**/node_modules", "**/coverage", "**/build", "**/lib", "**/typings"]), { files: ['**/*.ts'], extends: compat.extends( "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", ), plugins: { jsdoc, jest, "@typescript-eslint": typescriptEslint, }, languageOptions: { globals: { ...globals.node, ...jest.environments.globals.globals, }, parser: tsParser, ecmaVersion: 5, sourceType: "module", parserOptions: { project: "./tsconfig.json", }, }, settings: {}, rules: { "comma-dangle": [0], "guard-for-in": [0], "no-dupe-class-members": [0], "prefer-spread": [0], "prefer-rest-params": [0], "func-names": [0], "require-atomic-updates": [0], "no-empty": "off", "no-console": "off", "linebreak-style": [1, "unix"], "no-prototype-builtins": [0], "no-unused-vars": [0], "no-async-promise-executor": [0], "constructor-super": "error", "for-direction": ["error"], "getter-return": ["error"], "no-case-declarations": ["error"], "no-class-assign": ["error"], "no-compare-neg-zero": ["error"], "no-cond-assign": "error", "no-const-assign": ["error"], "no-constant-condition": ["error"], "no-control-regex": ["error"], "no-debugger": "error", "no-delete-var": ["error"], "no-dupe-args": ["error"], "no-dupe-keys": ["error"], "no-duplicate-case": ["error"], "no-empty-character-class": ["error"], "no-empty-pattern": ["error"], "no-ex-assign": ["error"], "no-extra-boolean-cast": ["error"], "no-extra-semi": ["error"], "no-fallthrough": "off", "no-func-assign": ["error"], "no-global-assign": ["error"], "no-inner-declarations": ["error"], "no-invalid-regexp": ["error"], "no-irregular-whitespace": "error", "no-misleading-character-class": ["error"], "no-mixed-spaces-and-tabs": ["error"], "no-new-symbol": ["error"], "no-obj-calls": ["error"], "no-octal": ["error"], "no-redeclare": "error", "no-regex-spaces": ["error"], "no-self-assign": ["error"], "no-shadow-restricted-names": ["error"], "no-sparse-arrays": "error", "no-this-before-super": ["error"], "no-undef": ["off"], "no-unexpected-multiline": ["error"], "no-unreachable": ["warn"], "no-unsafe-finally": "error", "no-unsafe-negation": ["error"], "no-unused-labels": "error", "no-useless-catch": ["error"], "no-useless-escape": ["error"], "no-with": ["error"], "require-yield": ["error"], "use-isnan": "error", "valid-typeof": "off", "jsdoc/tag-lines": "off", "@typescript-eslint/no-unnecessary-boolean-literal-compare": "off", "@typescript-eslint/no-unnecessary-type-assertion": "off", "@typescript-eslint/prefer-string-starts-ends-with": "off", "@typescript-eslint/prefer-regexp-exec": "off", "@typescript-eslint/adjacent-overload-signatures": "error", "@typescript-eslint/array-type": "off", "@typescript-eslint/require-await": "off", "@typescript-eslint/await-thenable": "error", "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/restrict-plus-operands": "off", "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "explicit", overrides: { accessors: "explicit", constructors: "off", }, }], "@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/member-delimiter-style": "off", "@typescript-eslint/camelcase": "off", "@typescript-eslint/member-ordering": "off", "@typescript-eslint/no-base-to-string": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-parameter-properties": "off", "@typescript-eslint/no-redundant-type-constituents": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unsafe-enum-comparison": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/prefer-for-of": "off", "@typescript-eslint/prefer-function-type": "off", "@typescript-eslint/prefer-namespace-keyword": "error", "@typescript-eslint/quotes": "off", "@typescript-eslint/no-require-imports": "off", "@/semi": ["error", "never"], "@typescript-eslint/triple-slash-reference": ["error", { path: "always", types: "prefer-import", lib: "always", }], "@typescript-eslint/unbound-method": "off", "@typescript-eslint/unified-signatures": "error", "arrow-body-style": "off", "arrow-parens": ["error", "as-needed"], camelcase: "off", complexity: "off", curly: "off", "dot-notation": "off", "eol-last": "off", eqeqeq: ["off", "always"], "id-blacklist": [ "error", "any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", ], "id-match": "error", "jsdoc/check-alignment": "error", "jsdoc/check-indentation": "error", "jsdoc/tag-lines": 1, "max-classes-per-file": "off", "new-parens": "error", "no-bitwise": "off", "no-caller": "error", "no-eval": "error", "no-invalid-this": "off", "no-magic-numbers": "off", "no-multiple-empty-lines": ["error", { max: 1, }], "no-new-wrappers": "error", "no-shadow": ["off", { hoist: "all", }], "no-template-curly-in-string": "off", "no-throw-literal": "error", "no-trailing-spaces": "error", "no-undef-init": "error", "no-underscore-dangle": "off", "no-unused-expressions": "off", "no-var": "error", "no-void": "off", "object-shorthand": "error", "one-var": ["error", "never"], "prefer-const": "off", "prefer-template": "off", "quote-props": ["error", "as-needed"], radix: "error", "space-before-function-paren": ["error", { anonymous: "never", asyncArrow: "always", named: "never", }], "spaced-comment": ["error", "always", { markers: ["/"], }], }, }, { files: ['src/__tests__/**/*.ts'], rules: { "@typescript-eslint/no-unsafe-function-type": "off" } } ]) ================================================ FILE: history.md ================================================ # Changelog Notable changes of coc.nvim: ## 2025-11-22 - Add configurable front end for reporting regular messages to the user, by using 'messageReportKind' which can be set to 'echo' or 'notification'. ## 2025-07-30 - Add configurable kind for dialog messages, through the use of the new configuration 'messageDialogKind' which can be set to 'menu', 'notification', or 'confirm'. ## 2025-07-18 - Add `extensionDependencies` support, declare dependencies on other extensions: `"extensionDependencies": ["extension-1", "extension-2"]` ## 2025-07-17 - Add notifications history, view with `:CocList notifications` ## 2025-06-11 - LSP 3.18 and latest vscode-languageclient features: - SnippetTextEdit support - Add `SnippetTextEdit` interface and namespace. - The `TextDocumentEdit.edits` array now allows `SnippetTextEdit`. - API `workspace.applyEdits()` now accepts `SnippetTextEdit`. - Introduced `StringValue` to represent snippet strings (kind: 'snippet', value: string). - Inline completion support, see `:h coc-inlineCompletion` - Add `InlineCompletionItem` `InlineCompletionList` interface and namespace. - Add enum `InlineCompletionTriggerKind`. - Add interfaces `InlineCompletionContext` `InlineCompletionItemProvider`. - Add method `languages.registerInlineCompletionItemProvider()`. - Inline completion support of LanguageClient. - Support `LanguageClient.getFeature('textDocument/inlineCompletion')`. - Support inline completion middleware `Middleware.provideInlineCompletionItems`. - Workspace‐edit metadata & applyEdit - Add `metadata` to `ApplyWorkspaceEditParams`. - Add `metadata` parameter to `workspace.applyEdit()`. - Richer `ErrorHandler.error()` and `ErrorHandler.closed()` return types - New interfaces `ErrorHandlerResult` and `CloseHandlerResult` (include `action`, optional `message`, optional `handled` flag) - `error()` and `closed()` may now return these richer results instead of bare enums. - Trace & output‐channel improvements. - Add `traceOutputChannel` to `LanguageClientOptions`. - Middleware can now intercept all requests and notifications via `sendRequest` and `sendNotification`. - Delayed “didOpen” notifications - Option `textSynchronization.delayOpenNotifications` was added to `LanguageClientOptions` so that `didOpen` can wait until a document is actually visible (or until another message is sent). - Text‐document‐content provider support - Registration type workspace/textDocumentContent to support custom‐ scheme content providers. - Support `middleware.provideTextDocumentContent` of LanguageClient. - Support `LanguageClient.getFeature('workspace/textDocumentContent')`. - Support `transport` of `Executable` server option. Transport could be `pipe` and `socket` ## 2025-06-02 - Add function keys support to notification popups on vim9. - Use notification dialogs with actions (instead of menu picker) when 'enableMessageDialog' is enabled. ## 2025-05-21 - Perform format on save after execute `editor.codeActionsOnSave`, the same as VSCode. ## 2025-05-20 - Add `winid` (current window ID) to `CursorHold` and `CursorHoldI` events handler. ## 2025-05-19 - Add `ModeChanged` event and `mode` property to `events`. ## 2025-05-13 - Change document highlight priority to -1 to avoid override search highlight on vim9. - `start_incl` and `end_incl` options works on neovim. ## 2025-05-08 - For terminal created by `coc#ui#open_terminal`, close the terminal window on terminal finish, make the behavior on vim9 the same as nvim. - Use lua and vim9 for highlight functions. ## 2025-05-04 - Execute python on snippet resolve, disable snippet synchronize on completion. - Change of none primary placeholder would not update placeholders with same index, like UltiSnip does. - Add API `snippetManager.insertBufferSnippets()`. ## 2025-05-03 - The performance with popupmenu navigation on vim9 have improved, for some cases, it's more than 10 times faster. - Break change: current line is not synchronized after use the pum API like `coc#pum#select()`, see `:h coc#pum#select()`, functions used as expr key-mappings should be not affected. - Break change: configuration `suggest.segmentChinese` replaced with `suggest.segmenterLocales`, see `:h coc-config-suggest-segmenterLocales`. - Add `CompleteStart` event to `events` module. ## 2025-04-25 - Add `-level` argument support to diagnostics list. - Make lines event send before TextChange events on vim9. ## 2025-04-23 - Add configuration `inlayHint.maximumLength`, default to `0` ## 2025-04-21 - Add `WindowVisible` event to `events`. - Add `onVisible()` support to `BufferSyncItem`. - Improve inlay hint: - Use lua and vim9 for virtual text api. - Add `coc#vtext#set()` for set multiple virtual texts. - Render all inlay hints for the first time. - Use `WindowVisible` event. ## 2025-04-18 - Add `nvim.createAugroup()`, `nvim.createAutocmd()` and `nvim.deleteAutocmd()`. - Add `buffer` `once` and `nested` support to `workspace.registerAutocmd()`. - Not throw error from autocmd callback, log the error instead. - Add configuration `editor.autocmdTimeout`. ## 2025-04-17 - Support `$COC_VIM_CHANNEL_ENABLE` for enable channel log on vim9. - Add `nvim.callVim()`, `nvim.evalVim()` and `nvim.exVim()`. ## 2025-04-15 - Support 'title' for configuration `suggest.floatConfig` and `suggest.pumFloatConfig`. - Use timer for `CocStatusChange` autocmd to avoid cursor vanish caused by `redraws`. - Use vim9 script for api.vim and refactor related functions. - Add `coc#compat#call` for call api functions on vim or neovim. - Add `special` to interface `KeymapOption` (vim9 only). ## 2025-04-06 - Add `cmd` option to interface `KeymapOption`. - Add `KeymapOption` support to `workspace.registerLocalKeymap()` ## 2025-04-04 - Add `right_gravity` property to `VirtualTextOption`. ## 2025-04-03 - Add `disposables` argument to `workspace.registerAutocmd()` - Change behavior for failure autocmd request, echo message instead of throw error. ## 2025-04-02 - Add method `window.getVisibleRanges()` to typings. - Break change: set `w:cocViewId` to upper case letters, see `:h w:cocViewId` ## 2025-04-01 - Add configuration `workspace.removeEmptyWorkspaceFolder` default to `false`. - Add configuration `editor.codeActionsOnSave`, similar to VSCode. ## 2025-03-31 - Change `placeHolder`to `placeholder` for `QuickPickOptions` like VSCode (old option still works). - Change interface `DocumentSelector`, could also be `DocumentFilter` or `string`, not only array of them. - Add `context.extensionUri` like VSCode. - Add `document` property to `DidChangeTextDocumentParams`, like VSCode. - Add `before()` and `after()` methods to `LinkedMap`, same as VSCode. - Add `onFocus` and `match()` to `DiagnosticPullOptions`. - Add `onFocus` to `DiagnosticPullMode` and export `DiagnosticPullMode` - Add interface `InlineValuesProvider`, `DiagnosticProvider` to typings. - Add missing properties to `LanguageClient` class, including `createDefaultErrorHandler()`, `state` `middleware` `isInDebugMode` `isRunning()` `dispose()` `getFeature()` ## 2025-03-29 - Add `bufnr` to `WinScrolled` event. ## 2025-03-28 - Improve vim9 highlight by vim9 script #5285. ## 2025-03-27 - Reworked snippets for UltiSnips options and actions support, see `:h coc-snippets` and #5282. ## 2025-03-13 - Add `coc.preferences.autoApplySingleQuickfix` configuration ## 2025-03-07 - Support `extensions.recommendations` configuration. - Support for UltiSnip options `t` `m` `s`. ## 2025-03-05 - Export method `workspace.fixWin32unixFilepath` for filepath convert. - Add commands `document.enableInlayHint` and `document.disableInlayHint`. - Refresh popup menu when completing incomplete sources. ## 2025-03-04 - Add VSCode command `workbench.action.openSettingsJson`. - Add `workspace.isTrusted` property. ## 2025-03-03 - Add command `workspace.openLocalConfig`. - Support vim built with win32unix enabled, including cygwin, git bash, WSL etc. ## 2025-02-24 - Configurations for file system watch, see `:h coc-config-fileSystemWatch`. ## 2025-02-23 - All global properties works with extensions #5222. - Return true or false for boolean option on vim (same as neovim). - Support completion sources using vim9script module. ## 2025-02-22 - QuickPick works with vim without terminal support. ## 2025-02-21 - To avoid unexpected signature help window close, signature help will be triggered after placeholder jump by default, when autocmd `CocJumpPlaceholder call CocActionAsync('showSignatureHelp')` not exists. - Support `global.formatFilepath` function for customize filepath displayed in symbols & location list. ## 2025-02-20 Use `extensions` section for extension related configurations. Deprecated configuration sections: `coc.preferences.extensionUpdateCheck`, `coc.preferences.extensionUpdateUIInTab` and `coc.preferences.silentAutoupdate`. ## 2025-01-03 - Add `diagnostic.displayByVimDiagnostic` configuration, set diagnostics to `vim.diagnostic` on nvim, and prevent coc.nvim's handler to display in virtualText/sign/floating etc. ## 2024-12-10 - Floating window can be set to fixed position, try `diagnostic.floatConfig` - `ensureDocument` and `hasProvider` support to accept specified bufnr ## 2024-11-29 - Increase `g:coc_highlight_maximum_count` default to 500 for better performance. - Add `uriConverter.code2Protocol` for extensions ## 2024-10-25 - Mention [davidosomething/coc-diagnostics-shim.nvim](https://github.com/davidosomething/coc-diagnostics-shim.nvim) as alternative to ALE for diagnostics display. ## 2024-08-28 - Add configuration `codeLens.display` ## 2024-08-20 - Add `CocAction('removeWorkspaceFolder')`. - Expanded the quick pick API in typings ## 2024-08-12 - Added `coc.preferences.formatterExtension` configuration ## 2024-07-04 - Added `NVIM_APPNAME` support ## 2024-06-27 - Added `inlayHint.position` configuration, with `inline` and `eol` options ## 2024-06-20 - Added `coc.preferences.extensionUpdateUIInTab` to open `CocUpdate` UI in tab ## 2024-05-29 - Break change: increase minimum vim/nvim version requirement - vim 9.0.0438 - nvim 0.8.0 ## 2024-05-14 - Added `suggest.reTriggerAfterIndent` to control re-trigger or not after indent changes ## 2024-05-07 - Allow `CocInstall` to install extension from Github in development mode ## 2024-04-12 - Change scope of codeLens configuration to `language-overridable` ## 2024-03-26 - Added new `--workspace-folder` argument for diagnostics lists - Added new `--buffer` argument for diagnostics lists ## 2024-02-28 - Increase `g:coc_highlight_maximum_count` default to 200 - Break change: semanticTokens highlight groups changed: - `CocSem + type` to `CocSemType + type` - `CocSem + modifier + type` to `CocSemTypeMod + type + modifier` ## 2024-03-06 - add `outline.autoHide` configuration to automatically hide the outline window when an item is clicked ## 2024-02-27 - Add `g:coc_disable_mappings_check` to disable key-mappings checking - Add `suggest.chineseSegments` configuration to control whether to divide Chinese sentences into segments or not ## 2023-09-02 - Support `g:coc_list_preview_filetype`. ## 2023-08-31 - Minimal node version changed from 14.14.0 to 16.18.0. - Inlay hint support requires neovim >= 0.10.0. - Removed configurations: - `inlayHint.subSeparator` - `inlayHint.typeSeparator` - `inlayHint.parameterSeparator` ## 2023-01-30 - Always show `cancellable` progress as notification without check `notification.statusLineProgress`. ## 2023-01-29 - Exclude `source` actions when request code actions with range. - Any character can be used for channel name. ## 2023-01-26 - Add escape support to `coc#status()`. ## 2023-01-24 - Add `encoding` and `CancellationToken` support for `runCommand` function. ## 2023-01-23 - Make `vscode.open` command work with file uri. - Cancel option for `workspace.registerExprKeymap()`. - Support `suggest.filterOnBackspace` configuration. ## 2023-01-22 - `maxRestartCount` configuration for configured language server. ## 2022-12-25 - Create symbol tree from SymbolInformation list. ## 2022-12-23 - Support `URI` as param for API `workspace.jumpTo()`. ## 2022-12-22 - Support popup window for window related APIs. ## 2022-12-21 - When create `CocSem` highlight group, replace invalid character of token types and token modifiers with underline. ## 2022-12-20 - Export `Buffer.setKeymap` and `Buffer.deleteKeymap` with vim and neovim support. - Make `workspace.registerLocalKeymap` accept bufnr argument. ## 2022-12-12 - Allow configuration of `window` scoped used by folder configuration file, like VSCode. - Add location support for `getHover` action. - Use unique id for each tab on vim. - Chinese word segmentation for keywords. ## 2022-12-05 - Add `switchConsole` method to `LanguageClient` ## 2022-12-03 - Add configuration `suggest.insertMode`. ## 2022-12-02 - Expand variables for string configuration value. ## 2022-11-30 - File fragment support for `workspace.jumpTo()`. - Support `g:coc_open_url_command`. - Support `contributes.configuration` from extension as array. ## 2022-11-29 - Add documentations for develop of coc.nvim extensions. - Remove unused variable `g:coc_channel_timeout`. ## 2022-11-28 - Placeholder and update value support for `InputBox` and `QuickPick`. - `triggerOnly` option property for vim completion source. - Export `getExtensionById` from `extensions` module. ## 2022-11-26 - Use CTRL-R expression instead of timer for pum related functions: - `coc#pum#insert()` - `coc#pum#one_more()` - `coc#pum#next()` - `coc#pum#prev()` - `coc#pum#stop()` - `coc#pum#cancel()` - `coc#pum#confirm()` ## 2022-11-25 - Avoid view change on list create. - Add configurations `links.enable` and `links.highlight`. - Use cursorline for list on neovim (to have correct highlight). - Fix highlight not work on neovim 0.5.0 by use `luaeval`. ## 2022-11-22 - Add command `document.toggleCodeLens`. ## 2022-11-21 - Add `CocAction('addWorkspaceFolder')`. ## 2022-11-20 - Support code lens feature on vim9. - `codeLens.subseparator` default changed to `|`, like VSCode. - Add configuration `coc.preferences.enableGFMBreaksInMarkdownDocument`, default to `true` - Add key-mappings `(coc-codeaction-selected)` and `(coc-codeaction-refactor-selected)`. ## 2022-11-19 - Create highlights after VimEnter. - Action 'organizeImport' return false instead of throw error when import code action not found. ## 2022-11-18 - Throw error when rpc request error, instead of echo message. ## 2022-11-13 - Plugin emit ready after extensions activated. ## 2022-11-12 - Not cancel completion when request for in complete sources. ## 2022-11-11 - Support filter and display completion items with different start positions. - Remove configuration `suggest.fixInsertedWord`, insert word would always be fixed. - Configuration `suggest.invalidInsertCharacters` default to line break characters. ## 2022-11-10 - Not reset 'Search' highlight on float window as it could be used. - Note remap `` on float preview window. - Add new action `feedkeys!` to list. - Add new configuration `list.floatPreview`. ## 2022-11-07 - Add API `CocAction('snippetInsert')` for snippet insert from vim plugin. - Snippet support for vim source, snippet item should have `isSnippet` to be `true` and `insertText` to be snippet text, when `on_complete` function exists, the snippet expand should be handled completion source. ## 2022-11-06 - `window.createQuickPick()` API that show QuickPick by default, call `show()` - Fix change value property for QuickPick not works. ## 2022-10-30 - Add configuration `colors.enable`, mark `colors.filetypes` deprecated. - Add command `document.toggleColors` for toggle colors of current buffer. - Changed filter of completion to use code from VSCode. - Add configuration `suggest.filterGraceful` ## 2022-10-39 - Add configuration `suggest.enableFloat` back. ## 2022-10-27 - Use `workspace.rootPatterns` replace `coc.preferences.rootPatterns`, old configuration still works when exists. - Store configurations with configuration registry. ## 2022-10-25 - Add `--height` support to `CocList`. ## 2022-10-24 - Use builtin static words source for snippet choices. - Remove configuration `"snippet.choicesMenuPicker"` - Remove unused internal functions `coc#complete_indent()` and `coc#_do_complete()` ## 2022-10-21 - Consider utf-16 code unit instead of unicode code point. - Add `coc#string#character_index()` `coc#string#byte_index()` and `coc#string#character_length()`. ## 2022-10-20 - Add `coc#pum#one_more()` ## 2022-10-19 - Trigger for trigger sources when no filter results available. ## 2022-10-18 - Change `suggest.maxCompleteItemCount` default to 256. ## 2022-10-17 - Set `g:coc_service_initialized` to `0` before service restart. - Show warning when diagnostic jump failed. - Use strwidth.wasm module for string display width. - Add API `workspace.getDisplayWidth`. ## 2022-10-15 - Add configuration `inlayHint.display`. ## 2022-10-07 - Use `CocFloatActive` for highlight active parameters. ## 2022-09-28 - Limit popupmenu width when exceed screen to &pumwidth, instead of change completion column. - Make escape of `${name}` for ultisnip snippets the same behavior as Ultisnip.vim. ## 2022-09-27 - Use fuzzy.wasm for native fuzzy match. - Add `binarySearch` and `isFalsyOrEmpty` functions for array. - `suggest.localityBonus` works like VSCode, using selection ranges. - Add and export `workspace.computeWordRanges`. - Rework keywords parse for better performance (parse changed lines only and use yield to reduce iteration). ## 2022-09-12 - All configurations are now scoped #4185 - No `onDidChangeConfiguration` event fired when workspace folder changed. - Deprecated configuration `suggest.detailMaxLength`, use `suggest.labelMaxLength` instead. - Deprecated configuration `inlayHint.filetypes`, use `inlayHint.enable` with scoped languages instead. - Deprecated configuration `semanticTokens.filetypes`, use `semanticTokens.enable` with scoped languages instead. - Use `workspaceFolderValue` instead of `workspaceValue` for `ConfigurationInspect` returned by `WorkspaceConfiguration.inspect()`. ## 2022-09-04 - Add configuration "snippet.choicesMenuPicker". ## 2022-09-03 - Send "WinClosed" event to node client. - Add `onDidFilterStateChange` and `onDidCursorMoved` to `TreeView`. - Support `autoPreview` for outline. ## 2022-09-02 - Support `diagnostic.virtualTextFormat`. - Add command `workspace.writeHeapSnapshot`. ## 2022-09-01 - Add configuration "suggest.asciiMatch" - Support `b:coc_force_attach`. ## 2022-08-31 - Add configuration "suggest.reversePumAboveCursor". - Use `DiagnosticSign*` highlight groups when possible. - Use `DiagnosticUnderline*` highlight groups when possible. ## 2022-08-30 - Export `LineBuilder` class. ## 2022-08-29 - Fix semanticTokens highlights unexpected cleared - Fix range of `doQuickfix` action. - Check reverse of `CocFloating`, use `border` and `Normal` highlight when reversed. - Make `CocInlayHint` use background of `SignColumn`. - Add command `document.toggleInlayHint`. ## 2022-08-28 - Make `CocMenuSel` use background of `PmenuSel`. - Snippet related configuration changed (old configuration still works until next release) - "coc.preferences.snippetStatusText" -> "snippet.statusText" - "coc.preferences.snippetHighlight" -> "snippet.highlight" - "coc.preferences.nextPlaceholderOnDelete" -> "snippet.nextPlaceholderOnDelete" - Add configuration `"list.smartCase"` - Add configurations for inlay hint - "inlayHint.refreshOnInsertMode" - "inlayHint.enableParameter" - "inlayHint.typeSeparator" - "inlayHint.parameterSeparator" - "inlayHint.subSeparator" ## 2022-08-27 - Avoid use `EasyMotion#is_active`, use autocmd to disable linting. - Show message when call hierarchy provider not found or bad position. ## 2022-08-26 - Remove `completeOpt` from `workspace.env`. - Add configuration `"diagnostic.virtualTextAlign"`. - Add warning when required features not compiled with vim. - Not echo error for semanticTokens request (log only). - Merge results form providers when possible. ## 2022-08-24 - Virtual text of suggest on vim9. - Virtual text of diagnostics on vim9. - Add configuration `inlayHint.filetypes`. - Inlay hint support on vim9. ## 2022-08-23 - Retry semanticTokens request on server cancel (LSP 3.17). - `RelativePattern` support for `workspace.createFileSystemWatcher()`. - `relativePatternSupport` for `DidChangeWatchedFiles` (LSP 3.17). - Not echo error on `doComplete()`. ## 2022-08-21 - Added `window.createFloatFactory()`, deprecated `FloatFactory` class. - Support `labelDetails` field of `CompleteItem`(LSP 3.17). - Added `triggerKind` to `CodeActionContext`, export `CodeActionTriggerKind`. ## 2022-08-20 - Support pull diagnostics `:h coc-pullDiagnostics`. - Break change: avoid extension overwrite builtin configuration defaults. - Change default value of configuration "diagnostic.format". - 'line' changes to 'currline' for `CocAction('codeAction')`. - Check NodeJS version on syntax error. ## 2022-08-10 - Change "notification.highlightGroup" default to "Normal". ## 2022-08-07 - Add configuration 'suggest.pumFloatConfig'. ## 2022-08-04 - Make diagnostic float window with the same background as CocFloating. ## 2022-08-03 - Add highlight group 'CocFloatingDividingLine'. ## 2022-08-01 - Use custom popup menu, #3862. - Use "first" instead of "none" for configuration `suggest.selection`. - Make "first" default for `suggest.selection`, like VSCode. - Add default blue color for hlgroup `CocMenuSel`. ## 2022-06-14 - Add highlight groups `CocListLine` and `CocListSearch`. ## 2022-06-11 - Add configuration "notification.disabledProgressSources" - Add "rounded" property to "floatConfig" ## 2022-06-04 - Add configuration `workspace.openOutputCommand`. - Log channel message of vim when `g:node_client_debug` enabled. ## 2022-05-30 - Disable `progressOnInitialization` for language client by default. ## 2022-05-28 - Support `repeat#set` for commands that make changes only. ## 2022-05-24 - Add transition and annotation support for `workspace.applyEdits()`. - Add command `workspace.undo` and `workspace.redo`. - Remove configuration `coc.preferences.promptWorkspaceEdit`. - Remove command `CocAction` and `CocFix`. ## 2022-05-22 - Check for previous position when not able to find completion match. - Add `content` support to `window.showMenuPicker()` ## 2022-05-17 - Add `QuickPick` module. - Add API `window.showQuickPick()` and `window.createQuickPick()`. ## 2022-05-16 - Add properties `title`, `loading` & `borderhighlight` to `InputBox` ## 2022-05-14 - Add `InputOption` support to `window.requestInput` - Add API `window.createInputBox()`. ## 2022-05-13 - Notification support like VSCode - Add configuration `notification.minProgressWidth` - Add configuration `notification.preferMenuPicker` - Support `source` in notification windows. ## 2022-05-07 - Show sort method as description in outline view. - Add configuration `outline.switchSortKey`, default to ``. - Add configuration `outline.detailAsDescription`, default to `true`. - Add variable `g:coc_max_treeview_width`. - Add `position: 'center'` support to `window.showMenuPicker()` ## 2022-05-06 - Use menu for `window.showQuickpick()`. - Add configuration `outline.autoWidth`, default to `true`. ## 2022-05-05 - Add key bindings to dialog (created by `window.showDialog()`) on neovim. ## 2022-05-04 - Add `languages.registerInlayHintsProvider()` for inlay hint support. ## 2022-04-25 - Add `LinkedEditing` support ## 2022-04-23 - Add `WinScrolled` event to events. ## 2022-04-20 - Select recent item when input is empty and selection is `recentUsedByPrefix`. - Add `coc#snippet#prev()` and `coc#snippet#next()`. - Add command `document.checkBuffer`. - Add `region` param to `window.diffHighlights()`. ## 2022-04-06 - `workspace.onDidOpenTextDocument` fire `contentChanges` as empty array when document changed with same lines. ## 2022-04-04 - Avoid `CompleteDone` cancel next completion. - Avoid indent change on `` and `` during completion. - Support `joinUndo` and `move` with `document.applyEdits()`. ## 2022-04-02 - Change `suggest.triggerCompletionWait` default to `0`. - Not trigger completion on `TextChangedP`. - Remove configuration `suggest.echodocSupport`. - Fix complettion triggered after ``. ## 2022-03-31 - Check buffer rename on write. ## 2022-03-30 - Improve words parse performance. - Remove configurations `coc.source.around.firstMatch` and `coc.source.buffer.firstMatch`. - Fix `coc.source.buffer.ignoreGitignore` not works - Check document reload on detach. ## 2022-03-29 - Add menu actions to refactor buffer. ## 2022-03-12 - Avoid use `` for cancel completion. ## 2022-03-05 - Make `WinClosed` event fires on `CursorHold` to support vim8. - Add events `TabNew` and `TabClose`. - Make outline reuse TreeView buffer. ## 2022-03-02 - Add ultisnip option to `snippetManager.insertSnippet()` and `snippetManager.resolveSnippet()`. - Support ultisnip regex option: `/a` (ascii option). - Support transform replacement of ultisnip, including: - Variable placeholders, `$0`, `$1` etc. - Escape sequence `\u` `\l` `\U` `\L` `\E` `\n` `\t` - Conditional replacement: `(?no:text:other text)` ## 2022-02-28 - Change `workspace.ignoredFiletypes` default value to `[]` ## 2022-02-24 - Add `window.activeTextEditor`, `window.visibleTextEditors`. - Add events `window.onDidChangeActiveTextEditor` `window.onDidChangeVisibleTextEditors`. - Add class `RelativePattern`. - Add `workspace.findFiles()`. ## 2022-02-23 - Add `workspace.openTextDocument()` - Add `Workspace.getRelativePath()`. - Add `window.terminals` `window.onDidOpenTerminal` `window.onDidCloseTerminal` and `window.createTerminal`. - Add `exitStatus` property to `Terminal`. - Support `strictEnv` in `TerminalOptions` on neovim. - Deprecated warning for `workspace.createTerminal()`, `workspace.onDidOpenTerminal` and `workspace.onDidCloseTerminal` ## 2022-02-18 - Clear all highlights created by coc.nvim before restart. - Support strike through for ansiparse. - Support `highlights` for `Documentation` in float window. ## 2022-02-17 - Change workspace configuration throw error when workspace folder can't be resolved. - Remove configuration `diagnostic.highlightOffset`. ## 2022-02-15 - Add `events.race`. - Change default `suggest.triggerCompletionWait` to 50. - Support trigger completion after indent fix. ## 2022-02-14 - Add `pumvisible` property to events. ## 2022-02-10 - Add shortcut support for `window.showMenuPicker()`. - Add configuration `dialog.shortcutHighlight` for shortcut highlight. - Add configuration `list.menuAction` for choose action by menu picker. ## 2022-02-09 - Add error log to `nvim_error_event`. - Add `nvim.lua()` which replace `nvim.executeLua()` to typings.d.ts. ## 2022-02-08 - Support `MenuItem` with disabled property for `window.showMenuPicker` - Support show disabled code actions in menu picker. ## 2022-02-07 - Change `:CocLocalConfig` to open configuration file of current workspace folder. ## 2022-02-05 - Support `version` from `textDocument/publishDiagnostics` notification's parameter. - Support `codeDescription` of diagnostics by add href to float window. - Support `showDocument` request from language server. - Support `label` from DocumentSymbolOptions in outline tree. - Support extra url use regexp under cursor with `openLink` action. - Support `activeParameter` from signature information. - Add `trimTrailingWhitespace`, `insertFinalNewline` and `trimFinalNewlines` to FormattingOptions. - Add configuration `links.tooltip`, default to `false`. ## 2022-02-04 - Add `--reverse` option to list. - Add `` key-mapping to cancel list in preview window (neovim only). ## 2022-02-02 - Remove `disableWorkspaceFolders` `disableDiagnostics` and `disableCompletion` from language client option. - Add configuration `documentHighlight.timeout`. - Add `tabPersist` option to `ListAction`. - Add `refactor` to `LocationList` ## 2022-01-30 - Add configuration `diagnostics.virtualTextLevel`. - Remove configuration `suggest.numberSelect` ## 2022-01-26 - Use `nvim_buf_set_text` when possible to keep extmarks. ## 2022-01-25 - Not trigger completion when filtered is succeed. - Move methods `workspace.getSelectedRange` `workspace.selectRange` to `window` module, show deprecated warning when using old methods. ## 2022-01-23 - Support semantic tokens highlights from range provider. ## 2022-01-22 - Not set `gravity` with api `nvim_buf_set_extmark` because highlight bug, wait neovim fix. - Support watch later created workspace folders for file events. ## 2022-01-21 - Changed semantic token highlight prefix from `CocSem_` to `CocSem`. - Changed semantic token highlight disabled by default, use configuration `semanticTokens.filetypes` - Add configuration `semanticTokens.filetypes`. - Add configuration `semanticTokens.highlightPriority`. - Add configuration `semanticTokens.incrementTypes`. - Add configuration `semanticTokens.combinedModifiers`. - Add configuration `workspace.ignoredFolders`. - Add configuration `workspace.workspaceFolderFallbackCwd`. - Add command `semanticTokens.refreshCurrent`. - Add command `semanticTokens.inspect`. - Add action `inspectSemanticToken`. - Rework command `semanticTokens.checkCurrent` to show highlight information. - Support semantic tokens highlight group composed with type and modifier. ## 2022-01-20 - Remove deprecated method `workspace.resolveRootFolder`. ## 2022-01-17 - Extend `buffer.updateHighlights` to support `priority`, `combine`, `start_incl` and `end_incl`. - Add configuration `diagnostic.highlightPriority`. - Add configuration `colors.filetypes` and `colors.highlightPriority`. ## 2022-01-16 - Add configuration `codeLens.position`. ## 2022-01-14 - Add configuration `suggest.selection`. ## 2022-01-13 - `codeLens.separator` now defaults to `""` and will be placed above lines on neovim >= 0.6.0 . - Add configurations 'diagnostic.locationlistLevel', 'diagnostic.signLevel', 'diagnostic.messageLevel'. ## 2022-01-12 - Add document.lineAt(), export TextLine class. - Upgrade node-client, support nvim.exec(). - Add documentHighlight.priority configuration. ## 2019-08-18 0.0.74 - feat(cursors): support multiple cursors. - feat(extensions): install missing extensions by CocInstall. - feat(extensions): add command `extensions.forceUpdateAll`. - feat(completion): rework preselect feature. - feat(extension): use request for fetch package info. - feat(language-client): support disableDynamicRegister configuration. - feat(list): paste from vim register support on insert mode #1088. - feat(plugin): add CocHasProvider(), close #1087. - refactor(outline): not exclude variables and callback. - refactor(diagnostic): remove timeout on InsertLeave. ## 2019-07-11 0.0.73 - fix(completion): fix map of number select - fix(languages): fix cursor position with snippet - fix(completion): fix cursor position with additionalTextEdits - fix(position): fix rangeOverlap check #961 - fix(list): not change guicursor when it's empty - fix(list): fix filter not work on loading - fix(list): fix custom location list command not work - fix(util): highlight & render on vim8 - fix(handler): fix getCommands - fix(handler): not check lastInsert on trigger signatureHelp - fix(handler): fix check of signature help trigger - fix(language-client): configuration for configured server, closes #930 - fix(diagnostic): clear diagnostics on filetype change - feat(plugin): add download & fetch modules - feat(plugin): add highlighter module - feat(refactor): add `(coc-refactor)` for refactor window - feat(extension): use mv module for folder rename - feat(extension): support install tagged extension - feat(extension): support custom extension root `g:coc_extension_root` - feat(handler): close signature float window on ')' - feat(list): support `g:coc_quickfix_open_command` - feat(list): add eval action - feat(list): add --tab list option - feat(list): use highlighter module for showHelp - feat(terminal): add noa on window jump - feat(terminal): support vim8 - feat(diagnostic): add diagnosticRelated support - feat(diagnostic): use text properties on vim8 - feat(handler): improve signature float window ## 2019-07-01 - feat(plugin): add CocStatusChange autocmd - feat(extension): support both npm and yarn. - feat(plugin): work on vim 8.0 - feat(extensions): add lock & doc actions to extension source - feat(extension): add proxy auth support (#920) - feat(source): not change startcol for file source - feat(completion): no numberSelect for number input - feat(extensions): Use yarn when npm not found - feat(completion): no popup for command line buffer - feat(plugin): support only for codeActions action - feat(task): debounce stdout - feat(plugin): add keymaps for selection ranges - feat(plugin): add function textobj - feat(list): restore window height, closes #905 - feat(handler): support signature.floatTimeout - feat(configuration): support change of workspace configuration - feat(diagnostic): add keymaps for jump error diagnostics - feat(plugin): delay start on gvim, fix #659 ## 2019-06-15 - feat(plugin): add popup support of vim - refactor(completion): improve float support - refactor(floating): remove unused code - refactor(workspace): replace find-up - refactor(handler): improve message for fold method - fix(virtualtext): invalid highlight tag (#874) - fix(snippets): fix plaintext check - fix(highlight): catch error of child_process.spawn - fix(highlight): use v:progpath, fix #871 - fix(floatFactory): escape feedkeys - fix(handler): fix getCurrentFunctionSymbol not work ## 2019-06-12 - feat(document): add getVar method - fix(util): not break selection on message - fix(workspace): fix jumpTo not work on vim8 - fix(completion): trigger completion with word character - refactor(handler): return boolean result - perf(workspace): improve jump performance - fix(util): Escape filename for jump (#862) - refactor(plugin): not show empty hover - feat(outline): ignore callback function - feat(workspace): support list of events with registerAutocmd - fix(workspace): fix jump with tab drop - refactor(language-client): change API of selectionRanges ## 2019-06-09 - **Break change** `CocHighlightText` link to `CursorColumn` by default. - **Break change** logger folder changed to `$XDG_RUNTIME_DIR` when exists. - Add `` and `` support for list, #825. - Add function `coc#add_command()`. - Add `disableDiagnostics` & `disableCompletion` to languageclient configuration. - Add `signature.triggerSignatureWait` configuration. - Add vim-repeat support for run command and quickfix. - Add preferred `codeAction` support. - Add `prompt.paste` action to list. - Add title as argument support for `codeAction` action. - Add `suggest.floatEnable` configuration. - Add `editor.action.organizeImport` command. - Add `:CocAction` and `:CocFix` commands. - Add `codeActions` action. - Fix issues with list. ## 2019-05-30 - **Break change** logger folder changed. - Add support of vim-repeat for `` keymaps. - Add `CocRegistNotification()` function. - Add argument to rename action. - Add `suggest.disableMenuShortcut` configuration. - Add glob support for root patterns. - Add `` keymap to list window. - Add shortcut in sources list. - Add `list.previewSplitRight` configuration. - Add `triggerOnly` property to source. - Add warning for duplicate extension. - Bug fixes. ## 2019-05-07 - **New feature** load extensions from coc-extensions folder. - Add `workspace.renameCurrentFile` command. - Add `FloatBuffer`, `FloatFactory` and `URI` to exports. - Add `resolveItem` support to list. - Fix prompt can't work when execute list action. - Fix ansiparser for empty color ranges. - Fix highlight only work with first 8 items. ## 2019-04-27 - **Break change** vim-node-rpc not required on vim. - **Break change** python not required on vim. - **Break change** complete items would refreshed after 500ms when not finished. - Add `additionalSchemes` for configured language server. - Add support for jumpCommand as false. - Fix `diagnostic.level` not work. ## 2019-04-09 - **Break change** `--strictMatch` option of list renamed to `--strict` - **Break change** `suggest.reloadPumOnInsertChar` support removed. - **Break change** no more binary release. - **Break change** logic for resolve workspace folder changed. - Add `Task` module. - Add `getCurrentFunctionSymbol` action. - Add `list.source.outline.ctagsFiletypes` setting. - Add `suggest.disableMenu` and `suggest.disableMenu` settings. - Add `equal` support for complete items. - Add support for do action with visual select lines of list. - Add expand tilder support for language server command. - Add switch matcher support to list. - Add select all support to list. - Add quickfix action to list. - Add `selectionRanges` of LSP. - Add load extensions for &rtp support. - Add `coc#on_enter()` for formatOnType and add new lines on enter. - Improve completion by support trigger completion when pumvisible. - Remove document check on `BufWritePre`. ## 2019-03-31 - **Break change** not using vim-node-rpc from npm modules any more. - **Break change** rename `_` to `CocRefresh`. - Fix wrong format options send to server. - Fix throw error when extension root not created. - Fix MarkedString not considered as markdown. - Fix echo message on vim exit. - Fix error throw on file watch. - Fix unexpected update of user configuration. ## 2019-03-28 - Add `workspace.resolveRootFolder`. - Add `diagnostic.joinMessageLines` setting. - Add `suggest.completionItemKindLabels` setting. - Add `memento` support for extension. - Add `workspace.getSelectedRange`. - Add `Terminal` module. - Add command `workbench.action.reloadWindow`. - Fix extension not activated by command. - Fix broken undo with floating window. - Fix document create possible wrong uri & filetype. - Improve highlight with floating window. ## 2019-03-24 - **Break change** make number input not trigger completion. - **Break change** make none keywords character doesn't filter completion. - Add functions for check snippet state. - Add setting `diagnostic.checkCurrentLine`. - Fix `signature.target` not work. - Fix flick of signature window. - Fix EPIPE error of node-client. - Fix wrong root of FileWatchSysmtem. ## 2019-03-19 - **Break change** signature settings now starts `signature`. - **Break change** default request timeout changed to 5s. - **Break change** `commands.executeCommand` return promise. - Add `coc.preferences.signatureHelpTarget`. - Add `diagnostic.maxWindowHeight` & `signature.maxWindowHeight`. - Add `diagnostic.enableSign`. - Add support for `$COC_NO_PLUGINS`. - Add keymaps: `(coc-float-hide)` and `(coc-float-jump)`. - Add `coc.preferences.enableFloatHighlight`. - Fix issues with floating window. - Fix critical performance issue on diff text. - Improve color of `CocHighlightText`. - Improve sort of complete items. - Improve extension list with version and open action. ## 2019-03-16 - **Break change** change vim config home on windows to '\$HOME/vimfiles'. - Add highlights to float windows. - Add CocLocationsAsync(). - Add support for `b:coc_suggest_disable`. - Add support for `b:coc_suggest_blacklist`. - Add setting `diagnostic.messageTarget`. - Add floating window support for signatures. - Fix issues with diagnostic float. - Fix info of completion item not shown. - Fix CocUpdateSync not work without service start. - Fix wrong indent spaces of snippets. ## 2019-03-11 - **Break change** change buffers instead of disk file for `workspace.applyEdits`. - **Break change** add config errors to diagnostic list instead of jump locations. - **Break change** hack for popup menu flicker is removed, use `suggest.reloadPumOnInsertChar` to enable it. - **Break change** use `nvim_select_popupmenu_item` for number select completion. - Add floating window for completion items. - Add floating window support for diagnostics. - Add floating window support for hover documentation. - Add `coc#on_enter()` for notify enter pressed. - Add setting `coc.preferences.useQuickfixForLocations`. - Add support of `g:coc_watch_extensions` for automatic reload extensions. - Add command: `editor.action.doCodeAction`. - Fix service on restarted on windows after rebuild. - Fix config of airline. - Fix relative path of watchman. - Improve Mru model. ## 2019-03-03 - **Break change** signature change of `workspace.registerKeymap`. - **Break change** `` of CocList can't be remapped any more. - **Break change** use `yarnpkg` command instead of `yarn` when possible. - **Break change** `noinsert` is removed from `completeopt` when `noselect` is enabled, `` would break line by default. - Add setting `diagnostic.refreshAfterSave`. - Add chinese documentation. - Add support of multiple line placeholder. - Fix edit of nested snippet placeholders. - Fix possible infinite create of documents. - Fix check for resume completion. ## 2019-02-25 - **Break change** default of `suggest.detailMaxLength` changed to 100. - **Break change** option of `workspace.registerKeymap` changed. - Add settings: `suggest.detailField`. - Add check for autocmd in health check. - Add trigger patterns support for complete sources. - Add support of `coc-snippets-expand-jump` - Add `source` option for completion start. - Add `sources.createSource` method. ## 2019-02-22 - **Break change** some configurations have been renamed, checkout #462. - **Break change** no longer automatic trigger for CursorHoldI #452. - **Break change** add preview option of `completeopt` according to `suggest.enablePreview`. - Add statusItem for CocUpdate. - Add `-sync` option for `:CocInstall` - Add support for floating preview window. - Add more module export. - Fix check of vim-node-rpc throw error. - Fix wrong line for TextEdit of complete item. - Fix diagnostics not cleared on service restart. ## 2019-02-17 - **Break change** completion resolve requires CompleteChanged autocmd. - **Break change** mapping of space on insert mode of list removed. - **Break change** kind of completion item use single letter. - Fix snippet not works on GUI vim. - Fix cursor vanish on vim by use timer hacks. - Fix behavior of list preview window. - Fix python check on vim. - Fix CocJumpPlaceholder not fired. - Fix vscode-open command not work. ## 2019-02-12 - **Break change** function `coc#util#clearmatches` signature changed. - Add check for python gtk module. - Add check for vim-node-rpc update error. - Fix source name of diagnostics. - Fix empty buffers created on preview. - Fix trigger of `CursorHoldI`. ## 2019-02-11 - **Break change:** internal filetype of settings file changed to jsonc. - **Break change:** `coc#util#install` changed to synchronize by default. - **Break change:** no document highlight would be added for colored symbol. - **Break change:** remove `coc.preferences.openResourceCommand`. - Add fallback rename implementation which rename symbols on current buffer. - Add command `:CocUpdateSync`. - Add `coc.preferences.detailMaxLength` for slice detail on completion menu. - Add cancel support for completion. - Add `ctags` as fallback of document symbols list. - Add default key-mappings for location actions. - Add python check on vim. - Add `disableSyntaxes` support for completion sources. - Add support for change `isProgress` of `StatusBarItem` - Add check of coc.nvim version for `CocUpdate` - Add `coc.preferences.previewAutoClose`, default true. - Add `workspace.add registerAutocmd`. - Fix highlight not cleared on vim - Fix health check of service state. - Fix CursorHoldI not triggered on neovim. - Fix sort of list not stable. ## 2019-02-04 - **Break change:** no messages when documentSymbol and workspaceSymbol provider not found. - Add support for configure sign in statusline. - Add help action for list. - Fix parse error on extensions update. - Fix wrong uri on windows. - Fix cancel list without close ui. - Improve startup time by remove jobwait. ## 2019-02-02 - **Break change:** extensions now update automatically, prompt is removed. - Add check for extension compatibility. - Add transform support for placeholder. - Add check for node version. - Add error check for list. - Add settings: `coc.preferences.diagnostic.virtualTextLines`. - Fix preview window not shown. - Fix highlight not cleared on vim. - Fix highlight commands of list block vim on start. - Improve extension load. - Improve list experience. ## 2019-01-28 - **Break change:** `coc.preferences.diagnostic.echoMessage` changed to enum. - Add mru support for commands and lists list. - Add `coc.preferences.diagnostic.refreshOnInsertMode` - Add `Mru` module. - Improve highlight for lists, support empty `filterLabel`. - Fix `findLocations` not work with nest locations. - Fix cursor position after apply additionalTextEdits. ## 2019-01-24 - **Break change:** python code for denite support moved to separated repo. - **Break change:** Quickfix list no longer used. - Add list support. - Add configuration: `coc.preferences.diagnostic.virtualText`. - Add watch for `&rtp` change. - Add support for configure `g:coc_user_config` and `g:coc_global_extensions` - Add support for send request to coc on vim start. - Add `g:coc_start_at_startup` support. - Add configuration: `coc.preferences.invalidInsertCharacters`. - Add configuration: `coc.preferences.snippetStatusText`. - Add `coc#_insert_key()` for insert keymap. - Add `workspace.registerExprKeymap()`. - Add detect for `vim-node-rpc` abnormal exist. - Add `requireRootPattern` to languageserver configuration. - Fix git check, always generate keywords. - Fix crash when `righleft` set to 1 on neovim. - Fix snippet position could be wrong. ## 2019-01-09 - **Break change:** throw error when languageserver id is invalid. - Add watcher for languageserver configuration change. - Fix possible invalid package.json. - Fix applyEdits not work sometimes. - Fix server still started when command search failed. - Fix log file not writeable. - Improve completion performance. ## 2019-01-03 - **Break change:** using of `g:rooter_patterns` is removed. - **Break change:** diagnostics would be updated in insert mode now. - Add configuration: `coc.preferences.rootPatterns` - Add `TM_SELECTED_TEXT` and `CLIPBOARD` support for snippets. - Fix check of latest insert char failed. - Fix highlight not cleared sometimes. ## 2019-01-01 - Fix issues with completion. ## 2018-12-31 - **Break change:** created keymaps use rpcrequest instead of rpcnotify. - **Break change:** snippets provider is removed, use `coc-snippets` for extension snippets. - Add command: `coc.action.insertSnippet` - Fix position of snippets. - Fix modifier of registered keymaps. - Fix completion triggered on complete done. - Fix closure function possible conflict. - Fix unexpected snippet cancel. - Fix document applyEdits, always use current lines. - Fix fail of yarn global command. - Fix check of changedtick on completion done. - Fix line used for textEdit of completion. - Fix snippet canceled by `formatOnType`. - Fix `CocJumpPlaceholder` not fired - Optimize content synchronize. ## 2018-12-27 - **Break change:** no more message on service ready. - **Break change:** vim source now registered as extension. - **Break change:** complete item sort have reworked. - **Break change:** request send to coc would throw when service not ready. - Add support for check current state on diagnostic update. - Add `env` opinion for registered command languageserver. - Add outputChannel for watchman. - Add `coc#_select_confirm()` for trigger select and confirm. - Add `coc.preferences.numberSelect`. - Add priority support for format provider. - Add `workspace.watchGlobal` and `workspace.watchOption` methods. - Fix cursor disappear on `TextChangedP` with vim. - Fix coc process not killed when update on windows. - Fix snippet broken on vim. - Fix support of startcol of completion result. - Fix `labelOffsetSupport` wrong position. - Fix flicking on neovim. - Fix unicide not considered as iskeyword. - Fix watchman client not initialized sometimes. - Improve performance for parse iskeyword. - Not echo message on vim exit. - Not send empty configuration change to languageserver. ## 2018-12-20 - **Break change** configuration for module language server, transport now require specified value. - **Break change** new algorithm for score complete items. - Add command `workspace.clearWatchman`. - Add `quickfixs`, `doCodeAction` and `doQuickfix` actions. - Add `g:vim_node_rpc_args` for debug purpose. - Add `coc#add_extension()` for specify extensions to install. - Fix clients not restarted on CocRestart. - Fix `execArgv` and `runtime` not work for node language server. - Fix detail of complete item not echoed sometimes. - Fix actions missing when registered with same clientId. - Fix issues with signature echo. - Fix uri is wrong with whitespace. - Improve highlight performance with `nvim_call_atomic`. ## 2018-12-17 - **Break change** `vim-node-rpc` now upgrade in background. - Add `ignoredRootPaths` to `languageserver` option. - Add detect of vim running state. - Add `client.vim` for create clients. - Fix possible wrong current line of `completeResolve`. - Fix snippet not work with `set virtualedit=all`. - Fix default timeout to 2000. - Fix file mode of log file. ## 2018-12-12 - **Break change** `fixInsertedWord` fix inserted word which ends with word after. - **Break change** `onCompleteSelect` is removed. - Add `workspace.registerKeymap` for register keymap. - Add match score for sort complete items. - Fix possible connection lost. - Fix priority of diagnostic signs. - Fix possible wrong uri. - Fix `RevealOutputChannelOn` not default to `never`. - Fix possible wrong line used for textEdit of complete item. - Fix possible wrong cursor position of snippet after inserted. ## 2018-12-08 - **Break change** default rootPath would be directory of current file, not cwd. - **Break change** codeLens feature now disabled by default. - **Break change** diagnostic prev/next now loop diagnostics. - Add support of neovim highlight namespace. - Add support for undo `additionalTextEdits` on neovim - Fix configuration resolve could be wrong. - Fix word of completion item could be wrong. - Fix rootPath could be null. - Fix highlight not cleared on restart. ## 2018-12-06 - **Break change** `RevealOutputChannelOn` of language client default to `never`. - Fix can't install on windows vim. - Fix `displayByAle` not clearing diagnostics. - Add check for `vim-node-rpc` update on vim. - Add `Resolver` module. - Improve apply `WorkspaceEdit`, support `0` as document version and merge edits for same document. ## 2018-12-05 - Add `CocJumpPlaceholder` autocmd. - Add `rootPatterns` to `languageserver` config. - Add setting: `coc.preferences.hoverTarget`, support use echo. - Add setting `coc.preferences.diagnostic.displayByAle` for use ale to display errors. - Add setting `coc.preferences.extensionUpdateCheck` for control update check of extensions. - Add `coc#config` for set configuration in vim. - Fix rootPath not resolved on initialize. - Fix possible wrong `tabSize` by use `shiftwidth` option. - Fix trigger of `documentColors` request. - Fix `vim-node-rpc` service not work on windows vim. - Fix `codeLens` not works. - Fix highlight of signatureHelp. - Fix watchman watching same root multiple times. - Fix completion throw undefined error. - Fix `open_terminal` not works on vim. - Fix possible connection lost by use notification when possible. - Fix process not terminated when connection lost. - Rework diagnostics with task sequence. - Rework configuration with more tests. ## 2018-11-28 - _Break change_ signature help reworked, vim API for echo signature changed. - Add `:CocInfo` command. - Add trigger for signature help after function expand. - Add echo message when provider not found for some actions. - Add support for `formatexpr` - Add support for locality bonus like VSCode. - Add support of `applyAdditionalLEdits` on item selected by `` - Add `coc.preferences.useQuickfixForLocations` - Add `coc.preferences.messageLevel` - Add support for trigger command which not registered by server. - Add `g:coc_denite_quickfix_action` - Fix insert unwanted word when trigger `commitCharacter`. - Fix rpc request throw on vim. - Fix `data` of complete item conflict. - Fix code action not work sometime. - Fix `coc.preferences.diagnostic.locationlist` not work. - Fix `coc.preference.preferCompleteThanJumpPlaceholder`. - Fix `workspace.jumpTo` not work sometime. - Fix line indent for snippet. - Fix trigger of `signatureHelp` and `onTypeFormat`. ## 2018-11-24 - **Break change** sources excluding `around`, `buffer` or `file` are extracted as extensions. - **Break change** custom source doesn't exist any more. - Add `coc.preferences.preferCompleteThanJumpPlaceholder` to make jump placeholder behavior as confirm completion when possible. - Add `CocDiagnosticChange` autocmd for force statusline update. - Add `onDidUnloadExtension` event on extension unload. - Fix `getDiagnosticsInRange`, consider all interactive ranges. - Fix completion throw when `data` on complete item is `string`. - Fix `commitCharacters` not works. - Fix workspace methods: `renameFile`, `deleteFile` and `resolveRoot`. - Fix textEdit of builtin sources not works. ## 2018-11-19 - **Break change** snippet support reworked: support nest snippets, independent session in each buffer and lots of fixes. - **Break change** diagnostic list now sort by severity first. - Add commands: `:CocUninstall` and `:CocOpenLog` - Add cterm color for highlights. - Add line highlight support for diagnostic. - Add `coc.preferences.fixInsertedWord` to make complete item replace current word. - Fix check confirm not works on vim sometimes. - Fix check of `vim-node-rpc`. - Fix preselect complete item not first sometimes. - Improve completion sort result by consider more abort priority and recent selected. - Improve colors module, only highlight current buffer and when buffer changed. - Improve `doc/coc.txt` ## 2018-11-13 - **Break change** default completion timeout changed to 2s. - **Break change** snippet session not canceled on `InsertLeave`, use `` in normal mode to cancel. - Add document color support. - Add CocAction 'pickColor' and 'colorPresentation'. - Add prompt for install vim-node-rpc module. - Add support for `inComplete` completion result. - Add status item for snippet session. - Add support for fix inserted text of snippet completion item. - Fix document highlight not cleared. - Fix cancel behavior of snippet. - Fix range check of edit on snippet session. - Fix check of completion confirm. - Fix highlight group 'CocHighlightWrite' not work. - Fix command `editor.action.rename` not works. - Fix throw error before initialize. - Fix `g:coc_node_path` not working. - Fix file source throw undefined error. - Improve logic of sorting completion items, strict match items comes first. ## 2018-11-07 - **Break change** word source removed from custom sources, enabled for markdown by default. - **Break change** ignore sortText when input.length > 3. - **Break change** show prompt for install `coc-json` when not found. - Fix document content synchronize could be wrong. - Fix filetype not converted on completion. - Fix complete item possible not resolved. - Improve document highlight, no highlight when cursor moved. - Improve completion score, use fuzzaldrin-plus replace fuzzaldrin. ## 2018-11-02 - **Break change** no items from snippets source when input is empty. - **Break change** `javascript.jsx` would changed to `javascriptreact` as languageId. - **Break change** `typescript.tsx` would changed to `typescriptreact` as languageId. - Add support for `commitCharacters` and `coc.preferences.acceptSuggestionOnCommitCharacter`. - Add setting: `coc.preferences.diagnostic.level`. - Add `g:coc_filetype_map` for customize mapping between filetype and languageId. - Add `g:coc_node_path` for custom node executable. - Add `workspaceFolders` feature to language client. - Add `~` to complete item of snippet source. - Add `onDidChangeWorkspaceFolder` event - Fix `eol` issue by check `eol` option. - Fix `workspace.document` could be null. - Fix `workspaceFolder` could be null. - Fix diagnostic for quickfix buffer. - Fix resolve of `coc.preferences.rootPath` ## 2018-10-29 - **Break change** diagnostic reworked, no refresh on insert mode. - **Break change** keep `sortText` on filter for better result. - **Break change** prefer trigger completion than filter, same as VSCode. - **Break change** filetype of document would be first part of `&filetype` split by `.`. - **Break change** prefer label as abbr for complete item. - Fix creating wrong `textEdit` for snippet. - Fix `startcol` of `CompleteResult` not working. - Fix `workspaceConfiguration.toJSON` return invalid result. - Fix `workspace.readFile` not synchronized with buffer. - Fix `workspace.rootPath` not resolved as expected. - Fix `CompletionItem` resolved multiple times. - Fix check of `latestInsert` on completion. - Fix `formatOnType` possible add unnecessary indent. - Fix document content synchronized on vim. - Fix confirm check of completion for all source. - Fix document possible register multiple times. - Fix completion always stopped when input is empty. - Add warning message when definition not found. - Add `redraw` after `g:coc_status` changed. - Remove change of `virtualedit` option of snippet. - Improved performance of filter completion items. ## 2018-10-25 - Fix `implementation` and `typeDefinition` of language client not working. - Fix `diffLines` return wrong range. - Fix `setqflist` and `setloclist` not works on vim. - Fix snippets and `additionalTextEdits` not works on vim. - Fix append lines not works on vim. - Fix highlight action not works on vim. - Fix null version of `TextDocumentIdentifier` not handled. - Add `workspace.registerTextDocumentContentProvider` for handle custom uri. - Add `workspace.createStatusBarItem` method. ## 2018-10-21 - **Break change**: `triggerAfterInsertEnter` now respect `minTriggerInputLength`. - Add `coc.preferences.minTriggerInputLength`. - Add command: `:CocCommand`. - Fix `position` of `provideCompletionItems`. - Fix content change not trigger after completion. - Fix default sorters & matchers of denite sources. - Fix `outputChannel` wrong `buftype`. - Fix completion not works with `textEdit` add new lines. - Fix first item not resolved when `noselect` is disabled - Remove using of `diff` module. ## 2018-10-18 - **Break change**: all buffers are created as document. - **Break change**: retrieve workspace root on document create. - Fix `uri` for all buffer types. - Fix bad performance on parse keywords. - Fix check of language client state. - Fix register of `renameProvider` - Fix `CocRequestAsync` not work. - Fix `workspace.openResource` error with `wildignore` option. - Fix output channel can't shown if hidden. - Fix extension activate before document create. - Add command `vscode.open` and `editor.action.restart`. - Add `workspace.requestInput` method. - Add support of `g:rooter_patterns` - Add `storagePath` to `ExtensionContext` - Add `workspace.env` property. - Add support of scoped configuration. - Disable buffer highlight on vim. ## 2018-10-14 - **Break change** API: `workspace.resoleModule` only does resolve. - **Break change** extension would still be loaded even if current coc version miss match. - **Break change** variables are removed from view of `Denite coc-symbols` - Fix `workspace.applyEdits` - Fix `console.log` throws in extension. - Fix invalid `workspace.root` with custom buffer schema. - Fix possible crash on neovim 0.3.1 by not attach terminal buffer. - Fix jump position not stored when jump to current buffer position. - Fix install function not works on vim. - Add support for custom uri schema for `workspace.jumpTo` and `workspace.openResource` - Add `workspace.findUp` for find up file of current buffer. - Add `env` option for custom language server config. - Add vim function: `CocRequest` and `CocRequestAsync` for send request to language server in vim. - Add `coc.preferences.parseKeywordsLimitLines` and `coc.preferences.hyphenAsKeyword` for buffer parse. - Rework completion for performance and accuracy. ## 2018-10-05 - **Break change**, `workspace.onDidChangeConfiguration` emit `ConfigurationChangeEvent` now. - Add `position` to function `coc#util#open_terminal`. - Improve performance of completion by use vim's filter when possible. - Fix service start multiple times. - Fix parse of `iskeyword` option, consider `@-@`. - Fix completion of snippet: cancel on line change. ## 2018-10-01 - Improved document `didChange` before trigger completion. - Add option `coc.preferences.triggerCompletionWait`, default 60. - Add watch for `iskeyword` change. - Fix snippet jump not works sometime. - Fix possible wrong `rootPath` of language server. - Fix highlight of highlight action not using terminal colors. - Fix detect for insert new line character. ## 2018-09-30 - Add quickfix source of denite and fzf - Add option `coc.preferences.rootPath` - Add option `revealOutputChannelOn` to language server. - Fix jump of placeholder. - Fix empty root on language server initialize. ## 2018-09-28 - **Break change**: `coc.preferences.formatOnType` default to `false`. - **Break change**: snippet completion disabled in `string` and `comment`. - Add support for register local extension. - Add title for commands in `Denite coc-command` - Fix prompt hidden by echo message. - Fix contribute commands not shown in denite interface. - Fix parse of `iskeyword`, support character range. - Fix `triggerKind` of completion. - Fix install extension from url not reloaded. ## 2018-09-27 - **Break change**: `:CocDisable` disabled all events from vim. - **Break change**: new snippet implementation. - Support multiple line snippet. - Support VSCode snippet extension. - Support completion of snippets from snippet extension. - Add highlight groups for different severity. - Add `coc.preferences.formatOnType` option. - Add `coc.preferences.snippets.enable` option. - Fix snippet not works as `insertText`. - Fix echo message with multiple lines. - Fix `signatureHelp` with `showcmd` disabled. - Fix location list cleared on `:lopen`. - Fix diagnostic info not cleared on `:CocDisable` - Fix diagnostic info not cleared on buffer unload. - Fix buffer highlight not cleared on `highlight` action. - Fix format on type not work as expected. ## 2018-09-24 - **Break change**: use `CursorMove` instead of `CursorHold` for diagnostic message. - **Break change**: direct move to diagnostic position would show diagnostic message without truncate. - **Break change**: snippet would be canceled when mode changed to normal, no mapping of `` any more. - Add format document on `insertLeave` when `onTypeFormat` is supported. - Add buffer operations on resource edit. - Add `uninstall` action for `Denite coc-extension`. - Fix active extension on command not working. - Fix delete file from resource edit not works. ## 2018-09-20 - Fix diagnostic check next offset for diagnostics. - Add `(coc-diagnostic-info)` for show diagnostic message without truncate. ## 2018-09-15 - Fix wrong configuration on update. - Fix install command with tag version. - Fix using of unsafe `new Buffer`. - Add support of trace format & resource operations. - Add support of json validation for extension. - Add support of format on save by `coc.preferences.formatOnSaveFiletypes` ## 2018-09-10 - Add `Denite coc-extension` for manage extensions. - Add actions for manage extension including `toggleExtension` `reloadExtension` `deactivateExtension` - Add check for extension update everyday. - Fix extensions using same process of coc itself. - Fix `configurationSection` should be null if none was specified. ## 2018-09-07 - **Break change**: all extension all separated from core, checkout [Using coc extension](https://github.com/neoclide/coc.nvim/wiki/Using-coc-extensions) - Fix `textDocumentSync` option not work when received as object. - Fix wrong diagnostic info when using multiple lint servers. - Use `CursorHold` for show diagnostic message. - Add option `coc.preferences.enableMessage` to disable showing of diagnostic message. - Add new events module for receive vim events. - Add support for `prepareRename`. - Add support for `CodeActionOptions` ## 2018-08-30 - Fix wrong `triggerKind` from VSCode. - Add `(coc-openlink)` for open link. - Add `typescript.jsx` as valid typescript type. ## 2018-08-23 - Fix sometimes client status invalid. - Add multiply provider support for all features. - Add `documentLink` support - Add `documentHighlight` support - Add `foldingRange` support - Add support of `documentSelector` same as VSCode ## 2018-08-21 - Fix diagnostic and arguments of tsserver. - Add `keepfocus` option for `open_terminal`. - Improve error catch of autocmds. - Add `onTypeFormat` feature for language server - Add `onTypeFormat` support for tsserver. - Refactor and more tests of workspace. - Fix `window/showMessageRequest` request. - Use `callAsync` for async request to vim. - Add `CocActionAsync` function send async request to server. ## 2018-08-17 - Fix exists terminal buffer not watched. - Fix buffer not attached after `edit!`. - Fix clean diagnostics of `tsserver.watchBuild` command. - Fix refresh of buffer. - Fix document not found on `BufEnter`. Use `rpcrequest` for `BufCreate` - Fix no permission of log file. Disable create log file for root user. - Add more command for tsserver: - `tsserver.reloadProjects` - `tsserver.openTsServerLog` - `tsserver.goToProjectConfig` - `tsserver.restart` - Add test for workspace. ## 2018-08-16 - Improved for tsserver: - Add `watchBuild` command for build current project with watch in terminal. - Support of untitled buffer - Support `projectRootPath` - Fix detach error of document. - Fix trigger characters not works for some source. - Fix document possible not sync before save. - Fix denite errors with 0 as result. - Fix wrong arguments of tsserver refactor command. - Use `drop` for workspace `openResource`. - Add clear coc signs on `:CocRestart`. - **Break change** all buffer types except `nofile` `help` and `quickfix` are watched for changes. ## 2018-08-15 - Fix filter of completion items on fast input. - Fix sometimes fails of include & neosnippet source. - Fix sometimes fails to find global modules. - Improve complete source initialization. - Always respect change of configuration. - Add ability to start standalone coc service for debugging. - Use `NVIM_LISTEN_ADDRESS=/tmp/nvim nvim` to start neovim. - Start coc server by command like `node bin/server.js` - Add ability to recover from unload buffer. Sometimes `bufReadPost` `BufEnter` could be not be fired on buffer create, check buffer on `CursorHold` and `TextChanged` to fix this issue. - Add tsserver features: `tsserver.formatOnSave` and `tsserver.organizeImportOnSave` Both default to false. - Add tests for completion sources. ## 2018-08-14 - Fix remote source not working. - Fix sort of completion items. - Fix EPIPE error from net module. - Add `tslint.lintProject` command. - Add config `coc.preferences.maxCompleteItemCount`. - Add `g:coc_auto_copen`, default to `1`. ## 2018-08-12 - **Break change** `:CocRefresh` replaced with `call CocAction('refreshSource')`. - Add support filetype change of buffer. - Add basic test for completion. - Improve loading speed, use child process to initialize vim sources. - Improve install.sh, install node when it doesn't exist. - Improve interface of workspace. - Fix loading of configuration content. ## 2018-08-11 - Fix configuration content not saved on change. - Fix thrown error on watchman not found. - Fix incompatible options of `child_process`. - Fix location list for diagnostics. - Reset on `BufWinEnter`. - Available for all windows of single buffer. - Use replace on change for coc location list. - Add debounce. - Fix signature help behaviour, truncate messages to not overlap. - Reworks sources use async import. ## 2018-08-10 - Fix dispose for all modules. - Add support for multiple `addWillSaveUntilListener`. - Fix `startcol` for json server. - Add support filetype `javascriptreact` for tsserver. ## 2018-08-09 - Add `coc#util#install` for installation. - Add `install.cmd` for windows. ## 2018-08-08 - Improved location list for diagnostics. - Add `internal` option to command. Commands registered by server are internal. - Add support for multiple save wait until requests. ## 2018-08-07 - Add `forceFullSync` to language server option. ## 2018-08-05 - Improve eslint extension to use workspaceFolder. - Fix watchman not works with multiple roots. - Add feature: dynamic root support for workspace. - **Break change** output channel of watchman is removed. ## 2018-08-04 - Fix order of document symbols. - Fix completion snippet with `$variable`. - Add feature: expand snippet on confirm. - Add feature: `(coc-complete-custom)` for complete custom sources. Default customs sources: `emoji`, `include` and `word` - **Break change** `emoji` `include` used for all filetypes by default. ## 2018-08-03 - Add command `:CocErrors` for debug. - Support `DocumentSymbol` for 'textDocument/documentSymbol' ## 2018-08-02 - Fix error of language client with unsupported schema. No document event fired for unsupported schema (eg: fugitive://) - Fix update empty configuration not works. ## 2018-07-31 - Improve file source triggered with dirname started path. ## 2018-07-30 - Fix source ultisnip not working. - Fix custom language client with command not working. - Fix wrong arguments passed to `runCommand` function. - Improve module install, add `sudo` for `npm install` on Linux. - Improve completion on backspace. - Completion is resumed when search is empty. - Completion is triggered when user try to fix search. ## 2018-07-29 - **Break change** all servers are decoupled from coc.nvim A prompt for download is shown when server not found. - **Break change** `vim-node-rpc` decoupled from coc.nvim A prompt would be shown to help user install vim-node-rpc in vim. - Add command `CocConfig` ## 2018-07-28 - Fix uncaught exception error on windows. - Use plugin root for assets resolve. - Fix emoji source not triggered by `:`. - Improve file source to recognize `~` as user home. ## 2018-07-27 - Prompt user for download server module with big extension like `vetur` and `wxml-langserver` - **Break change**, section of settings changed: `cssserver.[languageId]` moved to `[languageId]` For example: `cssserver.css` section is moved to `css` section. This makes coc settings of css languages the same as VSCode. - **Break change**, `stylelint` extension is disabled by default, add ```json "stylelint.enable": true, ``` to your `coc-settings.json` to enable it. User will be prompted to download server if `stylelint-langserver` is not installed globally. - **Break change**, `triggerAfterInsertEnter` is always `true`, add ```json "coc.preferences.triggerAfterInsertEnter": false, ``` to your `coc-settings.json` to disable it. - **Break change**, when `autoTrigger` is `always` completion would be triggered after completion item select. ## 2018-07-24 - better statusline integration with airline and lightline. ## 2018-07-23 - Coc service start much faster. - Add vim-node-rpc module. - **Break change** global function `CocAutocmd` and `CocResult` are removed. - Support Vue with vetur ## 2018-07-21 - Fix issue with `completeopt`. - Add source `neosnippet`. - Add source `gocode`. ## 2018-07-20 - Add documentation for language server debug. - Rework register of functions, avoid undefined function. ## 2018-07-19 - Fix error of `isFile` check. - Ignore undefined function on service start. ## 2018-07-17 - Add `coc.preference.jumpCommand` to settings. - Make coc service standalone. ## 2018-07-16 - Support arguments for `runCommand` action. - Add coc command `workspace.showOutput`. - Support output channel for language server. - Support `[extension].trace.server` setting for trace server communication. ## 2018-07-15 - Support location list for diagnostic. - Add tsserver project errors command. ## 2018-07-14 - Add support for `preselect` of complete item. - Add support for socket language server configuration. - Fix configured language server doesn't work. - Add `workspace.diffDocument` coc command. - Fix buffer sometimes not attached. - Improve completion of JSON extension. ## 2018-07-13 - **Break change:** `diagnostic` in setting.json changed to `diagnostic`. - Fix clearHighlight arguments. - Add eslint extension . - Fix snippet break with line have \$variable. - Use jsonc-parser replace json5. - Add `data/schema.json` for coc-settings.json. ## 2018-07-12 - Fix restart of tsserver not working. - Fix edit of current buffer change jumplist by using `:keepjumps`. ================================================ FILE: jest.js ================================================ const path = require('path') const os = require('os') const fs = require('fs') const tmpdir = process.env.TMPDIR = path.join(os.tmpdir(), 'coc-test') process.on('uncaughtException', err => { let msg = 'Uncaught exception: ' + err.stack console.error(msg) }) process.on('exit', () => { fs.rmdirSync(process.env.TMPDIR, {recursive: true, force: true}) }) module.exports = async () => { let dataHome = path.join(tmpdir, process.pid.toString()) fs.mkdirSync(dataHome, {recursive: true}) process.env.VIMRUNTIME = '' process.env.NODE_ENV = 'test' process.env.COC_NVIM = '1' process.env.COC_DATA_HOME = dataHome process.env.COC_VIMCONFIG = path.join(__dirname, 'src/__tests__') } ================================================ FILE: lua/coc/diagnostic.lua ================================================ local M = {} local ns = vim.api.nvim_create_namespace('coc-diagnostic') function M.refresh() vim.diagnostic.reset(ns) for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_is_loaded(bufnr) then local ok, items = pcall(vim.api.nvim_buf_get_var, bufnr, 'coc_diagnostic_map') if ok and type(items) == 'table' and vim.tbl_count(items) >= 0 then local diagnostics = {} for _, d in ipairs(items) do diagnostics[#diagnostics + 1] = { bufnr = bufnr, lnum = d.location.range.start.line, end_lnum = d.location.range['end'].line, col = d.location.range.start.character, end_col = d.location.range['end'].character, severity = d.level, message = d.message, source = d.source, code = d.code, namespace = ns, } end vim.diagnostic.set(ns, bufnr, diagnostics) end end end end return M ================================================ FILE: lua/coc/highlight.lua ================================================ local util = require('coc.util') local api = vim.api local M = {} local default_priority = 1024 local priorities = { CocListSearch = 2048, CocSearch = 2048 } local diagnostic_hlgroups = { CocUnusedHighlight = 0, CocDeprecatedHighlight = 1, CocHintHighlight = 2, CocInfoHighlight = 3, CocWarningHighlight = 4, CocErrorHighlight = 5 } local maxCount = vim.g.coc_highlight_maximum_count or 500 local n10 = vim.fn.has('nvim-0.10') == 1 and true or false -- 16 ms local maxTimePerBatchMs = 16 local function is_null(value) return value == nil or value == vim.NIL end local function is_enabled(value) return value == 1 or value == true end -- 0 based character index to 0 based byte index local function byte_index(text, character) if character == 0 then return 0 end local list = vim.str_utf_pos(text) local bytes = list[character + 1] if bytes == nil then return #text end return bytes - 1 end local function create_namespace(key) if type(key) == 'number' then if key == -1 then return api.nvim_create_namespace('') end return key end if type(key) ~= 'string' then error('Expect number or string for namespace key, got ' .. type(key)) end return api.nvim_create_namespace('coc-' .. key) end local function get_priority(hl_group, priority) if priorities[hl_group] ~= nil then return priorities[hl_group] end if type(priority) ~= 'number' then return default_priority end local n = diagnostic_hlgroups[hl_group] if n ~= nil then return priority + n end return priority end local function convert_item(item) if #item == 0 then local combine = 0 if item.combine or priorities[item.hlGroup] ~= nil then combine = 1 end return {item.hlGroup, item.lnum, item.colStart, item.colEnd, combine, item.start_incl, item.end_incl} end return item end local function getValuesWithPrefix(dict, prefix) local result = {} local prefixLength = #prefix for key, value in pairs(dict) do if type(key) == 'string' and string.sub(key, 1, prefixLength) == prefix then table.insert(result, value) end end return result end local function addHighlights(bufnr, ns, highlights, priority) for _, items in ipairs(highlights) do local converted = convert_item(items) local hlGroup = converted[1] local line = converted[2] local startCol = converted[3] local endCol = converted[4] local hlMode = is_enabled(converted[5]) and 'combine' or 'replace' if endCol == -1 then local text = vim.fn.getbufline(bufnr, line + 1)[1] or '' endCol = #text end priority = get_priority(hlGroup, priority) -- Error: col value outside range pcall(api.nvim_buf_set_extmark, bufnr, ns, line, startCol, { end_col = endCol, hl_group = hlGroup, hl_mode = hlMode, right_gravity = true, end_right_gravity = is_enabled(converted[7]), priority = math.min(priority, 4096) }) end end local function addHighlightTimer(bufnr, ns, highlights, priority, changedtick) if not api.nvim_buf_is_loaded(bufnr) then return nil end if api.nvim_buf_get_var(bufnr, 'changedtick') ~= changedtick then return nil end local total = #highlights local i = 1 local start = util.getCurrentTime() local next = {} while i <= total do local end_idx = math.min(i + maxCount - 1, total) local hls = vim.list_slice(highlights, i, end_idx) addHighlights(bufnr, ns, hls, priority) local duration = util.getCurrentTime() - start if duration > maxTimePerBatchMs and end_idx < total then next = vim.list_slice(highlights, end_idx + 1, total) break end i = end_idx + 1 end if #next > 0 then vim.defer_fn(function() addHighlightTimer(bufnr, ns, next, priority, changedtick) end, 10) end end -- Get single line extmarks -- @param bufnr - buffer number -- @param key - namespace id or key string. -- @param start_line - start line index, default to 0. -- @param end_line - end line index, default to -1. function M.get_highlights(bufnr, key, start_line, end_line) if not api.nvim_buf_is_loaded(bufnr) then return nil end start_line = type(start_line) == 'number' and start_line or 0 end_line = type(end_line) == 'number' and end_line or -1 local max = end_line == -1 and api.nvim_buf_line_count(bufnr) or end_line + 1 local ns = type(key) == 'number' and key or create_namespace(key) local markers = api.nvim_buf_get_extmarks(bufnr, ns, {start_line, 0}, {end_line, -1}, {details = true}) local res = {} for _, mark in ipairs(markers) do local id = mark[1] local line = mark[2] local startCol = mark[3] local details = mark[4] or {} local endCol = details.end_col if line < max then local delta = details.end_row - line if delta == 1 and endCol == 0 then local text = vim.fn.getbufline(bufnr, line + 1)[1] or '' endCol = #text elseif delta > 0 then endCol = -1 end if startCol >= endCol then api.nvim_buf_del_extmark(bufnr, ns, id) else table.insert(res, {details.hl_group, line, startCol, endCol, id}) end end end return res end -- Add single highlight -- @param id - buffer number or 0 for current buffer. -- @param key - namespace key or namespace number or -1. -- @param hl_group - highlight group. -- @param line - 0 based line index. -- @param col_start - 0 based col index, inclusive. -- @param col_end - 0 based col index, exclusive. -- @param opts - Optional table with priority and combine as boolean. function M.add_highlight(id, key, hl_group, line, col_start, col_end, opts) local bufnr = id == 0 and api.nvim_get_current_buf() or id opts = is_null(opts) and {} or opts if api.nvim_buf_is_loaded(bufnr) then local priority = get_priority(hl_group, opts.priority) if col_end == -1 then local text = vim.fn.getbufline(bufnr, line + 1)[1] or '' col_end = #text end if col_end > 0 and col_end > col_start then local ns = create_namespace(key) pcall(api.nvim_buf_set_extmark, bufnr, ns, line, col_start, { end_col = col_end, hl_group = hl_group, hl_mode = is_enabled(opts.combine) and 'combine' or 'replace', right_gravity = true, end_right_gravity = is_enabled(opts.end_incl), priority = math.min(priority, 4096) }) end end end -- Clear all namespaces with coc- namespace prefix. function M.clear_all() local namespaces = getValuesWithPrefix(api.nvim_get_namespaces(), 'coc-') local bufnrs = api.nvim_list_bufs() for _, bufnr in ipairs(bufnrs) do if api.nvim_buf_is_loaded(bufnr) then for _, ns in ipairs(namespaces) do api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) end end end end -- Remove extmarks by id table. -- @param bufnr - buffer number -- @param key - namespace id or key string. -- @param ids - table with ids of extmarks. function M.del_markers(bufnr, key, ids) if api.nvim_buf_is_loaded(bufnr) then local ns = create_namespace(key) for _, id in ipairs(ids) do api.nvim_buf_del_extmark(bufnr, ns, id) end end end -- Add highlights to buffer. -- @param bufnr - buffer number -- @param key - namespace id or key string. -- @param highlights - highlight items, item could be HighlightItem dict or number list. -- @param priority - optional priority function M.set_highlights(bufnr, key, highlights, priority) if api.nvim_buf_is_loaded(bufnr) then local changedtick = api.nvim_buf_get_var(bufnr, 'changedtick') local ns = create_namespace(key) addHighlightTimer(bufnr, ns, highlights, priority, changedtick) end end -- Clear namespace highlights of region. -- @param id - buffer number or 0 for current buffer. -- @param key - namespace id or key string. -- @param start_line - start line index, default to 0. -- @param end_line - end line index, default to -1. function M.clear_highlights(id, key, start_line, end_line) local bufnr = id == 0 and api.nvim_get_current_buf() or id start_line = type(start_line) == 'number' and start_line or 0 end_line = type(end_line) == 'number' and end_line or -1 if api.nvim_buf_is_loaded(bufnr) then local ns = create_namespace(key) api.nvim_buf_clear_namespace(bufnr, ns, start_line, end_line) end end -- Update highlights of specific region. -- @param id - buffer number or 0 for current buffer. -- @param key - namespace id or key string. -- @param highlights - highlight items, item could be HighlightItem dict or number list. -- @param start_line - start line index, default to 0. -- @param end_line - end line index, default to -1. -- @param priority - optional priority. -- @param changedtick - optional buffer changedtick. function M.update_highlights(id, key, highlights, start_line, end_line, priority, changedtick) local bufnr = id == 0 and api.nvim_get_current_buf() or id start_line = type(start_line) == 'number' and start_line or 0 end_line = type(end_line) == 'number' and end_line or -1 if api.nvim_buf_is_loaded(bufnr) then local ns = create_namespace(key) local tick = api.nvim_buf_get_var(bufnr, 'changedtick') if type(changedtick) == 'number' and changedtick ~= tick then return end api.nvim_buf_clear_namespace(bufnr, ns, start_line, end_line) addHighlightTimer(bufnr, ns, highlights, priority, tick) end end -- Update namespace highlights of whole buffer. -- @param bufnr - buffer number. -- @param key - namespace id or key string. -- @param highlights - highlight items, item could be HighlightItem dict or number list. -- @param priority - optional priority. -- @param changedtick - optional buffer changedtick. function M.buffer_update(bufnr, key, highlights, priority, changedtick) if api.nvim_buf_is_loaded(bufnr) then local ns = create_namespace(key) api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) if #highlights > 0 then local tick = api.nvim_buf_get_var(bufnr, 'changedtick') if type(changedtick) ~= 'number' or tick == changedtick then local winid = vim.fn.bufwinid(bufnr) if winid == -1 then addHighlightTimer(bufnr, ns, highlights, priority, tick) else local info = vim.fn.getwininfo(winid)[1] local topline = info.topline local botline = info.botline if topline <= 5 then addHighlightTimer(bufnr, ns, highlights, priority, tick) else local curr_hls = {} local other_hls = {} for _, hl in ipairs(highlights) do local lnum = hl[2] ~= nil and hl[2] + 1 or hl.lnum + 1 if lnum >= topline and lnum <= botline then table.insert(curr_hls, hl) else table.insert(other_hls, hl) end end vim.list_extend(curr_hls, other_hls) addHighlightTimer(bufnr, ns, curr_hls, priority, tick) end end end end end end -- Add highlights to LSP ranges -- @param id - buffer number or 0 for current buffer. -- @param key - namespace id or key string. -- @param hl_group - highlight group. -- @param ranges - LSP range list. -- @param opts - Optional table with priority and clear, combine as boolean. function M.highlight_ranges(id, key, hl_group, ranges, opts) local bufnr = id == 0 and api.nvim_get_current_buf() or id if is_null(opts) or type(opts) ~= 'table' then opts = {} end if api.nvim_buf_is_loaded(bufnr) then if opts.clear then local ns = create_namespace(key) api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) end local highlights = {} for _, range in ipairs(ranges) do local sp = range['start'] local ep = range['end'] local lines = vim.fn.getbufline(bufnr, sp.line + 1, ep.line + 1) for index=sp.line,ep.line,1 do local line = lines[index - sp.line + 1] or '' if #line > 0 then local colStart = index == sp.line and byte_index(line, sp.character) or 0 local colEnd = index == ep.line and byte_index(line, ep.character) or #line if colEnd > colStart then local combine = is_enabled(opts.combine) and 1 or 0 table.insert(highlights, {hl_group, index, colStart, colEnd, combine, opts.start_incl, opts.end_incl}) end end end end if #highlights > 0 then local priority = type(opts.priority) == 'number' and opts.priority or 4096 M.set_highlights(bufnr, key, highlights, priority) end end end -- Use matchaddpos to add highlights to window. -- @param id - window id, or 0 for current window. -- @param buf - buffer number, or 0 for current buffer. -- @param ranges - LSP ranges. -- @param hlGroup - highlight group. -- @param priority - Optional priority, default to 99. function M.match_ranges(id, buf, ranges, hl_group, priority) local winid = id == 0 and api.nvim_get_current_win() or id local bufnr = buf == 0 and vim.fn.winbufnr(winid) or buf if not api.nvim_win_is_valid(winid) or vim.fn.winbufnr(winid) ~= bufnr then return {} end local ids = {} local pos = {} for _, range in ipairs(ranges) do local sp = range['start'] local ep = range['end'] local lines = vim.fn.getbufline(bufnr, sp.line + 1, ep.line + 1) for index=sp.line,ep.line,1 do local line = lines[index - sp.line + 1] or '' if #line > 0 then local colStart = index == sp.line and byte_index(line, sp.character) or 0 local colEnd = index == ep.line and byte_index(line, ep.character) or #line if colEnd > colStart then table.insert(pos, {index + 1, colStart + 1, colEnd - colStart}) end end end end local count = #pos if count > 0 then priority = type(priority) == 'number' and priority or 99 local opts = {window = winid} if count < 9 or n10 then ---@diagnostic disable-next-line: param-type-mismatch table.insert(ids, vim.fn.matchaddpos(hl_group, pos, priority, -1, opts)) else local group = {} for i=1,count,8 do for j = i,math.min(i+7, count) do table.insert(group, pos[j]) end ---@diagnostic disable-next-line: param-type-mismatch table.insert(ids, vim.fn.matchaddpos(hl_group, group, priority, -1, opts)) group = {} end end end return ids end return M ================================================ FILE: lua/coc/text.lua ================================================ local api = vim.api local M = {} local function splitText(text, col) return text:sub(1, col - 1), text:sub(col) end local function copy(t) local list = {} for k, v in pairs(t) do list[k] = v end return list end local function insertList(target, insert, linePos, colPos) local result = {} for i = 1, #target do if i < linePos or i > linePos then table.insert(result, target[i]) else local before, after = splitText(target[i], colPos) for j = 1, #insert do local text = insert[j] if j == 1 then text = before .. text end if j == #insert then text = text .. after end table.insert(result, text) end end end return result end local function lcsDiff(str1, str2) -- 计算最长公共子序列 local function lcs(a, b) local matrix = {} for i = 0, #a do matrix[i] = {} for j = 0, #b do if i == 0 or j == 0 then matrix[i][j] = 0 elseif a:sub(i, i) == b:sub(j, j) then matrix[i][j] = matrix[i - 1][j - 1] + 1 else matrix[i][j] = math.max(matrix[i - 1][j], matrix[i][j - 1]) end end end local result = '' local i, j = #a, #b while i > 0 and j > 0 do if a:sub(i, i) == b:sub(j, j) then result = a:sub(i, i) .. result i = i - 1 j = j - 1 elseif matrix[i - 1][j] > matrix[i][j - 1] then i = i - 1 else j = j - 1 end end return result end local common = lcs(str1, str2) local result = {} local i1, i2, ic = 1, 1, 1 while ic <= #common do -- 处理str1中不在公共序列的部分 while i1 <= #str1 and str1:sub(i1, i1) ~= common:sub(ic, ic) do table.insert(result, { type = '-', char = str1:sub(i1, i1) }) i1 = i1 + 1 end -- 处理str2中不在公共序列的部分 while i2 <= #str2 and str2:sub(i2, i2) ~= common:sub(ic, ic) do table.insert(result, { type = '+', char = str2:sub(i2, i2) }) i2 = i2 + 1 end -- 添加公共字符 if ic <= #common then table.insert(result, { type = '=', char = common:sub(ic, ic) }) i1 = i1 + 1 i2 = i2 + 1 ic = ic + 1 end end -- 处理剩余字符 while i1 <= #str1 do table.insert(result, { type = '-', char = str1:sub(i1, i1) }) i1 = i1 + 1 end while i2 <= #str2 do table.insert(result, { type = '+', char = str2:sub(i2, i2) }) i2 = i2 + 1 end return result end -- Try find new col in changed text -- Not 100% correct, but works most of the time. -- Return nil when not found local function findNewCol(text, col, newText) local before, after = splitText(text, col) if #before == 0 then return 1 end if #after == 0 then return #newText + 1 end if #before <= #after and string.sub(newText, 1, #before) == before then return col end if string.sub(newText, -#after) == after then return #newText - #after + 1 end local diff = lcsDiff(text, newText) local used = 1 local index = 1 for _, item in ipairs(diff) do if item.type == '-' then used = used + #item.char elseif item.type == '+' then index = index + #item.char elseif item.type == '=' then local total = used + #item.char if total >= col then local plus = col - used used = col index = index + plus else used = total index = index + #item.char end end if used == col then break end if used > col then return nil end end return used == col and index or nil end local function findInsert(arr1, arr2, linePos, colPos) local l1 = #arr1 local l2 = #arr2 if l1 < l2 or linePos < 1 or linePos > #arr1 then return nil end arr2 = copy(arr2) for i = 1, #arr1 - linePos, 1 do local a = arr1[l1 - i + 1] local idx = l2 - i + 1 local b = arr2[idx] if b == nil then return nil end if a ~= b then return nil end table.remove(arr2, idx) end local before, after = splitText(arr1[linePos], colPos) local last = arr2[#arr2] if #after > 0 and last:sub(-#after) ~= after then return nil end arr2[#arr2] = last:sub(1, - #after - 1) for index, value in ipairs(arr2) do local text = arr1[index] if index < #arr2 and text ~= value then return nil end if index == #arr2 and text:sub(1, #value) ~= value then return nil end end local pos = {} pos.line = #arr2 pos.col = #(arr2[#arr2]) + 1 local inserted = {} for i = pos.line, linePos, 1 do if i == pos.line then local text = arr1[i]:sub(pos.col) table.insert(inserted, text) elseif i == linePos then table.insert(inserted, before) else table.insert(inserted, arr1[i]) end end return pos, inserted end local function findStringDiff(oldStr, newStr, reverseFirst) local len1, len2 = #oldStr, #newStr local maxLen = math.max(len1, len2) -- 先从后往前找差异 if reverseFirst then local end1, end2 = len1, len2 while end1 >= 1 and end2 >= 1 do local c1 = oldStr:sub(end1, end1) local c2 = newStr:sub(end2, end2) if c1 ~= c2 then break end end1 = end1 - 1 end2 = end2 - 1 end -- 如果完全相同 if end1 < 1 and end2 < 1 then return nil end -- 然后从前往后找差异 local start = 1 while start <= end1 and start <= end2 do local c1 = oldStr:sub(start, start) local c2 = newStr:sub(start, start) if c1 ~= c2 then break end start = start + 1 end return { startPos = start, endPosOld = end1, inserted = newStr:sub(start, end2) } else local start = 1 while start <= maxLen do local c1 = start <= len1 and oldStr:sub(start, start) or nil local c2 = start <= len2 and newStr:sub(start, start) or nil if c1 ~= c2 then break end start = start + 1 end -- 如果完全相同 if start > maxLen then return nil end -- 然后从后往前找差异 local end1, end2 = len1, len2 while end1 >= start and end2 >= start do local c1 = oldStr:sub(end1, end1) local c2 = newStr:sub(end2, end2) if c1 ~= c2 then break end end1 = end1 - 1 end2 = end2 - 1 end return { startPos = start, endPosOld = end1, inserted = newStr:sub(start, end2) } end end local function replaceSubstring(original, startPos, endPos, replacement) local prefix = original:sub(1, startPos - 1) local suffix = original:sub(endPos + 1) return prefix .. replacement .. suffix end local function hasConflict(diff1, diff2) -- 如果任一diff为nil(无变化),则无冲突 if not diff1 or not diff2 then return false end -- 获取两个diff的修改范围 local start1, end1 = diff1.startPos, diff1.endPosOld local start2, end2 = diff2.startPos, diff2.endPosOld -- 处理删除的情况(endPos可能小于startPos) end1 = math.max(end1, start1 - 1) end2 = math.max(end2, start2 - 1) -- 检查范围是否重叠 local overlap = not (end1 < start2 or end2 < start1) return overlap end local function diffApply(original, current, newText, reverseFirst) local diff1 = findStringDiff(original, current, reverseFirst) local diff2 = findStringDiff(original, newText, not reverseFirst) if hasConflict(diff1, diff2) then diff1 = findStringDiff(original, current, not reverseFirst) diff2 = findStringDiff(original, newText, reverseFirst) end if diff1 == nil or diff2 == nil or hasConflict(diff1, diff2) then return nil end local result if diff1.startPos < diff2.startPos then result = replaceSubstring(original, diff2.startPos, diff2.endPosOld, diff2.inserted) result = replaceSubstring(result, diff1.startPos, diff1.endPosOld, diff1.inserted) else result = replaceSubstring(original, diff1.startPos, diff1.endPosOld, diff1.inserted) result = replaceSubstring(result, diff2.startPos, diff2.endPosOld, diff2.inserted) end return result end -- Change single line by use nvim_buf_set_text -- 1 based line number, current line, applied line function M.changeLineText(bufnr, lnum, current, applied) local diff = findStringDiff(current, applied) if diff ~= nil then local lineIdx = lnum - 1 api.nvim_buf_set_text(bufnr, lineIdx, diff.startPos - 1, lineIdx, diff.endPosOld, {diff.inserted}) end end -- Check if new line insert. -- Check text change instead of insert only. -- Check change across multiple lines. function M.set_lines(bufnr, changedtick, originalLines, replacement, startLine, endLine, changes, cursor, col, linecount) if not api.nvim_buf_is_loaded(bufnr) then return nil end local delta = 0 local column = vim.fn.col('.') if type(col) == 'number' then delta = column - col end local applied = nil local idx = 0 local currentBuf = api.nvim_get_current_buf() == bufnr local current = currentBuf and vim.fn.getline('.') or '' if currentBuf and api.nvim_buf_get_var(bufnr, 'changedtick') > changedtick then local lnum = vim.fn.line('.') idx = lnum - startLine if idx >= 1 then local original = originalLines[idx] local count = vim.fn.line('$') if count ~= linecount then -- Check content insert before cursor. if count > linecount then local currentLines = api.nvim_buf_get_lines(bufnr, startLine, endLine + count - linecount, false) -- Cursor not inside if currentLines[idx] == nil then return nil end -- Compare to original lines, find insert position, text local pos, inserted = findInsert(currentLines, originalLines, idx, column) if pos ~= nil then local newText = replacement[pos.line] if newText == nil then return nil end local colPos = findNewCol(originalLines[pos.line], pos.col, newText) if colPos == nil then return nil end replacement = insertList(replacement, inserted, pos.line, colPos) endLine = endLine + count - linecount changes = vim.NIL else return nil end else return nil end else -- current line changed if original ~= nil and original ~= current then local newText = replacement[idx] if newText ~= nil then if newText == original then applied = current else applied = diffApply(original, current, newText, column > #current/2) end end end end end end if applied ~= nil then replacement[idx] = applied if #replacement < 30 then -- use nvim_buf_set_text to keep extmarks for i = 1, math.min(#replacement, #originalLines) do local text = idx == i and current or originalLines[i] M.changeLineText(bufnr, startLine + i, text, replacement[i]) end if #replacement > #originalLines then local newLines = vim.list_slice(replacement, #originalLines + 1) api.nvim_buf_set_lines(bufnr, endLine, endLine, false, newLines) elseif #originalLines > #replacement then api.nvim_buf_set_lines(bufnr, startLine + #replacement, endLine, false, {}) end else api.nvim_buf_set_lines(bufnr, startLine, endLine, false, replacement) end else if type(changes) == 'table' and #changes > 0 then -- reverse iteration for i = #changes, 1, -1 do local item = changes[i] api.nvim_buf_set_text(bufnr, item[2], item[3], item[4], item[5], item[1]) end else api.nvim_buf_set_lines(bufnr, startLine, endLine, false, replacement) end end if currentBuf and type(cursor) == 'table' then vim.fn.cursor({cursor[1], cursor[2] + delta}) end end return M ================================================ FILE: lua/coc/util.lua ================================================ local M = {} local unpackFn = unpack if unpackFn == nil then unpackFn = table.unpack end M.unpack = unpackFn function M.sendErrorMsg(msg) vim.defer_fn(function() vim.api.nvim_call_function('coc#rpc#notify', {'nvim_error_event', {0, 'Lua ' .. _VERSION .. ':'.. msg}}) end, 10) end function M.getCurrentTime() return os.clock() * 1000 end local function errorHandler(err) local traceback = debug.traceback(2) return err .. "\n" .. traceback end -- catch the error and send notification to NodeJS function M.call(module, func, args) local m = require(module) local method = m[func] local result = nil if method ~= nil then local ok, err = xpcall(function () result = method(unpackFn(args)) end, errorHandler) if not ok then local msg = 'Error on ' .. module .. '[' .. func .. ']" ' .. err M.sendErrorMsg(msg) error(msg) end else local msg = 'Method "' .. module .. '[' .. func .. ']" not exists' M.sendErrorMsg(msg) error(msg) end return result end return M ================================================ FILE: lua/coc/vtext.lua ================================================ local api = vim.api local M = {} local n10 = vim.fn.has('nvim-0.10') local maxCount = vim.g.coc_highlight_maximum_count or 500 local function addVirtualText(bufnr, ns, opts, pre, priority) local align = opts.text_align or 'after' local config = { hl_mode = opts.hl_mode or 'combine', right_gravity = opts.right_gravity } local column = opts.col or 0 if align == 'above' or align == 'below' then if #pre == 0 then config.virt_lines = { opts.blocks } else local list = { {pre, 'Normal'}} vim.list_extend(list, opts.blocks) config.virt_lines = { list } end if align == 'above' then config.virt_lines_above = true end else config.virt_text = opts.blocks if n10 and column ~= 0 then config.virt_text_pos = 'inline' elseif align == 'right' then config.virt_text_pos = 'right_align' elseif type(opts.virt_text_win_col) == 'number' then config.virt_text_win_col = opts.virt_text_win_col config.virt_text_pos = 'overlay' elseif align == 'overlay' then config.virt_text_pos = 'overlay' else config.virt_text_pos = 'eol' end if type(opts.virt_lines) == 'table' then config.virt_lines = opts.virt_lines config.virt_text_pos = 'overlay' end end if type(priority) == 'number' then config.priority = math.min(priority, 4096) end local col = column ~= 0 and column - 1 or 0 -- api.nvim_buf_set_extmark(bufnr, ns, opts.line, col, config) -- Error: col value outside range pcall(api.nvim_buf_set_extmark, bufnr, ns, opts.line, col, config) end -- This function is called by buffer.setVirtualText function M.add(bufnr, ns, line, blocks, opts) local pre = '' if opts.indent == true then local str = vim.fn.getbufline(bufnr, line + 1)[1] or '' pre = string.match(str, "^%s*") or '' end local conf = {line = line, blocks = blocks} for key, value in pairs(opts) do conf[key] = value end addVirtualText(bufnr, ns, conf, pre, opts.priority) end -- opts.line - Zero based line number -- opts.blocks - List with [text, hl_group] -- opts.hl_mode - Default to 'combine'. -- opts.col - nvim >= 0.10.0, 1 based. -- opts.virt_text_win_col -- opts.text_align - Could be 'after' 'right' 'below' 'above', converted on neovim. -- indent - add indent when using 'above' and 'below' as text_align local function addVirtualTexts(bufnr, ns, items, indent, priority) if #items == 0 then return nil end local buflines = {} local start = 0 if indent then start = items[1].line local endLine = items[#items].line buflines = api.nvim_buf_get_lines(bufnr, start, endLine + 1, false) or {} end for _, opts in ipairs(items) do local pre = indent and string.match(buflines[opts.line - start + 1], "^%s*") or '' addVirtualText(bufnr, ns, opts, pre, priority) end end local function addVirtualTextsTimer(bufnr, ns, items, indent, priority, changedtick) if not api.nvim_buf_is_loaded(bufnr) then return nil end if vim.fn.getbufvar(bufnr, 'changedtick', 0) ~= changedtick then return nil end if #items > maxCount then local markers = {} local next = {} vim.list_extend(markers, items, 1, maxCount) vim.list_extend(next, items, maxCount, #items) addVirtualTexts(bufnr, ns, markers, indent, priority) vim.defer_fn(function() addVirtualTextsTimer(bufnr, ns, next, indent, priority, changedtick) end, 10) else addVirtualTexts(bufnr, ns, items, indent, priority) end end function M.set(bufnr, ns, items, indent, priority) local changedtick = vim.fn.getbufvar(bufnr, 'changedtick', 0) addVirtualTextsTimer(bufnr, ns, items, indent, priority, changedtick) end return M ================================================ FILE: package.json ================================================ { "name": "coc.nvim-master", "version": "0.0.82", "description": "LSP based intellisense engine for neovim & vim8.", "main": "./build/index.js", "engines": { "node": ">=16.18.0" }, "type": "commonjs", "scripts": { "lint": "eslint . --quiet --concurrency auto", "lint:typecheck": "tsc -p tsconfig.json", "build": "node esbuild.js", "test": "./node_modules/.bin/jest --forceExit", "test-build": "./node_modules/.bin/jest --coverage --forceExit", "prepare": "node esbuild.js" }, "repository": { "type": "git", "url": "git+https://github.com/neoclide/coc.nvim.git" }, "keywords": [ "complete", "neovim" ], "author": "Qiming Zhao ", "bugs": { "url": "https://github.com/neoclide/coc.nvim/issues" }, "homepage": "https://github.com/neoclide/coc.nvim#readme", "jest": { "globals": { "__TEST__": true }, "projects": [ "" ], "watchman": false, "clearMocks": true, "globalSetup": "./jest.js", "testEnvironment": "node", "coveragePathIgnorePatterns": [ "/src/__tests__/*" ], "moduleFileExtensions": [ "ts", "tsx", "json", "mjs", "js" ], "transform": { "^.+\\.tsx?$": [ "@swc/jest" ] }, "testRegex": "src/__tests__/.*\\.(test|spec)\\.ts$", "coverageReporters": [ "text", "lcov" ], "coverageDirectory": "./coverage/" }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", "@swc/jest": "^0.2.39", "@types/cli-table": "^0.3.4", "@types/debounce": "^1.2.4", "@types/fb-watchman": "^2.0.6", "@types/follow-redirects": "^1.14.4", "@types/jest": "^30.0.0", "@types/node": "^18.19.25", "@types/semver": "^7.7.1", "@types/unidecode": "^1.1.0", "@types/uuid": "^9.0.8", "@types/which": "^3.0.4", "@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/parser": "^8.57.0", "esbuild": "^0.27.4", "eslint": "10.0.3", "eslint-formatter-unix": "^9.0.1", "eslint-plugin-jest": "^29.15.0", "eslint-plugin-jsdoc": "^62.8.0", "globals": "^16.5.0", "jest": "30.3.0", "typescript": "^5.5.4", "vscode-languageserver": "^10.0.0-next.16" }, "dependencies": { "@chemzqm/neovim": "^6.3.6", "ansi-styles": "^5.2.0", "bytes": "^3.1.2", "cli-table": "^0.3.11", "content-disposition": "^0.5.4", "debounce": "^1.2.1", "decompress-response": "^6.0.0", "fast-diff": "^1.3.0", "fb-watchman": "^2.0.2", "follow-redirects": "^1.15.11", "glob": "10.5", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "iconv-lite": "^0.7.2", "jsonc-parser": "^3.3.1", "marked": "^7.0.5", "minimatch": "^9.0.4", "semver": "^7.7.4", "strip-ansi": "^6.0.1", "tar": "^7.5.11", "tslib": "^2.8.1", "unidecode": "^1.1.0", "unzip-stream": "^0.3.4", "uuid": "^9.0.1", "vscode-languageserver-protocol": "^3.17.6-next.16", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.6-next.6", "vscode-uri": "^3.1.0", "which": "^4.0.0" } } ================================================ FILE: plugin/coc.lua ================================================ if vim.fn.has('nvim-0.10') then vim.api.nvim_create_autocmd({ 'BufEnter' }, { callback = function() require('coc.diagnostic').refresh() end, }) vim.api.nvim_create_autocmd('User', { pattern = 'CocDiagnosticChange', callback = function() require('coc.diagnostic').refresh() end, }) end ================================================ FILE: plugin/coc.vim ================================================ scriptencoding utf-8 let s:is_vim = !has('nvim') let s:is_gvim = s:is_vim && has("gui_running") if exists('g:did_coc_loaded') || v:version < 800 finish endif function! s:checkVersion() abort let l:unsupported = 0 if get(g:, 'coc_disable_startup_warning', 0) != 1 if s:is_vim let l:unsupported = !has('patch-9.0.0438') else let l:unsupported = !has('nvim-0.8.0') endif if l:unsupported == 1 echohl Error echom "coc.nvim requires at least Vim 9.0.0438 or Neovim 0.8.0, but you're using an older version." echom "Please upgrade your (neo)vim." echom "You can add this to your vimrc to avoid this message:" echom " let g:coc_disable_startup_warning = 1" echom "Note that some features may error out or behave incorrectly." echom "Please do not report bugs unless you're using at least Vim 9.0.0438 or Neovim 0.8.0." echohl None sleep 2 endif endif endfunction call s:checkVersion() let g:did_coc_loaded = 1 let g:coc_service_initialized = 0 let s:root = expand(':h:h') if get(g:, 'coc_start_at_startup', 1) && !s:is_gvim call coc#rpc#start_server() endif function! CocTagFunc(pattern, flags, info) abort " tagfunc can't be set in the sandbox mode, preload the following functions silent! call coc#cursor#move_to() silent! call coc#string#character_index() if a:flags !=# 'c' " use standard tag search return v:null endif return coc#rpc#request('getTagList', []) endfunction " Used by popup prompt on vim function! CocPopupCallback(bufnr, arglist) abort if len(a:arglist) == 2 if a:arglist[0] == 'confirm' call coc#rpc#notify('PromptInsert', [a:arglist[1], a:bufnr]) elseif a:arglist[0] == 'exit' " notify exit for vim terminal prompt to ensure cleanup call coc#rpc#notify('PromptExit', [a:bufnr]) execute 'silent! bd! '.a:bufnr "call coc#rpc#notify('PromptUpdate', [a:arglist[1]]) elseif a:arglist[0] == 'change' let text = a:arglist[1] let current = getbufvar(a:bufnr, 'current', '') if text !=# current call setbufvar(a:bufnr, 'current', text) let cursor = term_getcursor(a:bufnr) let info = { \ 'lnum': cursor[0], \ 'col': cursor[1], \ 'line': text, \ 'changedtick': 0 \ } call coc#rpc#notify('CocAutocmd', ['TextChangedI', a:bufnr, info]) endif elseif a:arglist[0] == 'send' call coc#rpc#notify('PromptKeyPress', [a:bufnr, a:arglist[1]]) endif endif endfunction function! CocAction(name, ...) abort if !get(g:, 'coc_service_initialized', 0) throw 'coc.nvim not ready when invoke CocAction "'.a:name.'"' endif return coc#rpc#request(a:name, a:000) endfunction function! CocHasProvider(name, ...) abort let bufnr = empty(a:000) ? bufnr('%') : a:1 return coc#rpc#request('hasProvider', [a:name, bufnr]) endfunction function! CocActionAsync(name, ...) abort return s:AsyncRequest(a:name, a:000) endfunction function! CocRequest(...) abort return coc#rpc#request('sendRequest', a:000) endfunction function! CocNotify(...) abort return coc#rpc#request('sendNotification', a:000) endfunction function! CocRegisterNotification(id, method, cb) abort call coc#on_notify(a:id, a:method, a:cb) endfunction " Deprecated, use CocRegisterNotification instead function! CocRegistNotification(id, method, cb) abort call coc#on_notify(a:id, a:method, a:cb) endfunction function! CocLocations(id, method, ...) abort let args = [a:id, a:method] + copy(a:000) return coc#rpc#request('findLocations', args) endfunction function! CocLocationsAsync(id, method, ...) abort let args = [a:id, a:method] + copy(a:000) return s:AsyncRequest('findLocations', args) endfunction function! CocRequestAsync(...) return s:AsyncRequest('sendRequest', a:000) endfunction function! s:AsyncRequest(name, args) abort let Cb = empty(a:args)? v:null : a:args[len(a:args) - 1] if type(Cb) == 2 if !coc#rpc#ready() call Cb('service not started', v:null) else call coc#rpc#request_async(a:name, a:args[0:-2], Cb) endif return '' endif call coc#rpc#notify(a:name, a:args) return '' endfunction function! s:CommandList(...) abort let list = coc#rpc#request('commandList', a:000) return join(list, "\n") endfunction function! s:ExtensionList(...) abort let stats = CocAction('extensionStats') call filter(stats, 'v:val["isLocal"] == v:false') let list = map(stats, 'v:val["id"]') return join(list, "\n") endfunction function! s:SearchOptions(...) abort let list = ['-e', '--regexp', '-F', '--fixed-strings', '-L', '--follow', \ '-g', '--glob', '--hidden', '--no-hidden', '--no-ignore-vcs', \ '--word-regexp', '-w', '--smart-case', '-S', '--no-config', \ '--line-regexp', '--no-ignore', '-x'] return join(list, "\n") endfunction function! s:LoadedExtensions(...) abort let list = CocAction('loadedExtensions') return join(list, "\n") endfunction function! s:InstallOptions(...)abort let list = ['-terminal', '-sync'] return join(list, "\n") endfunction function! s:OpenConfig() let home = coc#util#get_config_home(1) if !isdirectory(home) echohl MoreMsg echom 'Config directory "'.home.'" does not exist, create? (y/n)' echohl None let confirm = nr2char(getchar()) redraw! if !(confirm ==? "y" || confirm ==? "\r") return else call mkdir(home, 'p') end endif execute 'edit '.fnameescape(home.'/coc-settings.json') call coc#rpc#notify('checkJsonExtension', []) endfunction function! s:get_color(item, fallback) abort let t = type(a:item) if t == 1 return a:item endif if t == 4 let item = get(a:item, 'gui', {}) let color = get(item, &background, a:fallback) return type(color) == 1 ? color : a:fallback endif return a:fallback endfunction function! s:AddAnsiGroups() abort let color_map = {} let colors = ['#282828', '#cc241d', '#98971a', '#d79921', '#458588', '#b16286', '#689d6a', '#a89984', '#928374'] let names = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'grey'] for i in range(0, len(names) - 1) let name = names[i] if exists('g:terminal_ansi_colors') let color_map[name] = s:get_color(get(g:terminal_ansi_colors, i, colors[i]), colors[i]) else let color_map[name] = get(g:, 'terminal_color_'.i, colors[i]) endif endfor try for name in keys(color_map) let foreground = toupper(name[0]).name[1:] let foregroundColor = color_map[name] for key in keys(color_map) let background = toupper(key[0]).key[1:] let backgroundColor = color_map[key] exe 'hi default CocList'.foreground.background.' guifg='.foregroundColor.' guibg='.backgroundColor endfor exe 'hi default CocListFg'.foreground. ' guifg='.foregroundColor. ' ctermfg='.foreground exe 'hi default CocListBg'.foreground. ' guibg='.foregroundColor. ' ctermbg='.foreground endfor catch /.*/ " ignore invalid color endtry endfunction function! s:CreateHighlight(group, fg, bg) abort let cmd = coc#hlgroup#compose(a:fg, a:bg) if !empty(trim(cmd)) exe 'hi default '.a:group.' '.cmd else exe 'hi default link '.a:group.' '.a:fg endif endfunction function! s:OpenDiagnostics(...) abort let height = get(a:, 1, 0) call coc#rpc#request('fillDiagnostics', [bufnr('%')]) if height execute ':lopen '.height else lopen endif endfunction function! s:Disable() abort if get(g:, 'coc_enabled', 0) == 0 return endif autocmd! coc_nvim call coc#rpc#notify('detach', []) echohl MoreMsg echom '[coc.nvim] Event disabled' echohl None let g:coc_enabled = 0 endfunction function! s:Autocmd(...) abort if !get(g:, 'coc_workspace_initialized', 0) return endif call coc#rpc#notify('CocAutocmd', a:000) endfunction function! s:HandleBufEnter(bufnr) abort if s:is_vim call coc#api#Buf_flush(a:bufnr) endif call s:Autocmd('BufEnter', a:bufnr) endfunction function! s:HandleCharInsert(char, bufnr) abort call s:Autocmd('InsertCharPre', a:char, a:bufnr) endfunction function! s:HandleTextChangedI(event, bufnr) abort if s:is_vim " make sure lines event before changed event. call coc#api#Buf_flush(a:bufnr) endif call s:Autocmd(a:event, a:bufnr, coc#util#change_info()) endfunction function! s:HandleTextChanged(bufnr) abort if s:is_vim " make sure lines event before changed event. call coc#api#Buf_flush(a:bufnr) endif call s:Autocmd('TextChanged', a:bufnr, getbufvar(a:bufnr, 'changedtick')) endfunction function! s:HandleInsertLeave(bufnr) abort call coc#pum#close() call s:Autocmd('InsertLeave', a:bufnr) endfunction function! s:HandleWinScrolled(winid, event) abort if getwinvar(a:winid, 'float', 0) call coc#float#nvim_scrollbar(a:winid) endif if !empty(a:event) let opt = get(a:event, 'all', {}) if get(opt, 'topline', 0) == 0 && get(opt, 'height', 0) == 0 " visible region not changed. return endif for key in keys(a:event) let winid = str2nr(key) if winid != 0 let info = getwininfo(winid) if !empty(info) call s:Autocmd('WinScrolled', winid, info[0]['bufnr'], [info[0]['topline'], info[0]['botline']]) endif endif endfor else " v:event not exists on old version vim9 let info = getwininfo(a:winid) if !empty(info) call s:Autocmd('WinScrolled', a:winid, info[0]['bufnr'], [info[0]['topline'], info[0]['botline']]) endif endif endfunction function! s:HandleWinClosed(winid) abort call coc#float#on_close(a:winid) call coc#notify#on_close(a:winid) call s:Autocmd('WinClosed', a:winid) endfunction function! s:SyncAutocmd(...) if !get(g:, 'coc_workspace_initialized', 0) return endif call coc#rpc#request('CocAutocmd', a:000) endfunction function! s:VimLeavePre() abort let g:coc_vim_leaving = 1 call s:Autocmd('VimLeavePre') if s:is_vim && exists('$COC_NVIM_REMOTE_ADDRESS') " Helps to avoid connection error. call coc#rpc#close_connection() return endif if get(g:, 'coc_node_env', '') ==# 'test' return endif if s:is_vim call timer_start(1, { -> coc#client#kill('coc')}) endif endfunction function! s:VimEnter() abort if coc#rpc#started() if !exists('$COC_NVIM_REMOTE_ADDRESS') call coc#rpc#notify('VimEnter', [join(coc#compat#list_runtime_paths(), ",")]) endif elseif get(g:, 'coc_start_at_startup', 1) call coc#rpc#start_server() endif call s:Highlight() endfunction function! s:Enable(initialize) if get(g:, 'coc_enabled', 0) == 1 return endif let g:coc_enabled = 1 sign define CocCurrentLine linehl=CocMenuSel sign define CocListCurrent linehl=CocListLine sign define CocTreeSelected linehl=CocTreeSelected if s:is_vim call coc#api#Tabpage_ids() endif augroup coc_nvim autocmd! if !v:vim_did_enter autocmd VimEnter * call s:VimEnter() else call s:Highlight() endif if s:is_vim if exists('##DirChanged') autocmd DirChanged * call s:Autocmd('DirChanged', getcwd()) endif if exists('##TerminalOpen') autocmd TerminalOpen * call s:Autocmd('TermOpen', +expand('')) endif autocmd CursorMoved list:///* call coc#list#select(bufnr('%'), line('.')) autocmd TabNew * call coc#api#Tabpage_ids() else autocmd DirChanged * call s:Autocmd('DirChanged', get(v:event, 'cwd', '')) autocmd TermOpen * call s:Autocmd('TermOpen', +expand('')) autocmd WinEnter * call coc#float#nvim_win_enter(win_getid()) endif if exists('##CompleteChanged') autocmd CompleteChanged * call timer_start(1, { -> execute('if pumvisible() | call coc#pum#close() | endif')}) endif autocmd CursorHold * call coc#float#check_related() if exists('##WinClosed') autocmd WinClosed * call s:HandleWinClosed(+expand('')) elseif exists('##TabEnter') autocmd TabEnter * call coc#notify#reflow() endif if exists('##WinScrolled') autocmd WinScrolled * call s:HandleWinScrolled(+expand(''), v:event) endif autocmd ModeChanged * call s:Autocmd('ModeChanged', v:event) autocmd TabNew * call s:Autocmd('TabNew', coc#compat#tabnr_id(tabpagenr())) autocmd TabClosed * call s:Autocmd('TabClosed', coc#compat#call('list_tabpages', [])) autocmd WinLeave * call s:Autocmd('WinLeave', win_getid()) autocmd WinEnter * call s:Autocmd('WinEnter', win_getid()) autocmd BufWinLeave * call s:Autocmd('BufWinLeave', +expand(''), bufwinid(+expand(''))) autocmd BufWinEnter * call s:Autocmd('BufWinEnter', +expand(''), win_getid(), coc#window#visible_range(win_getid())) autocmd FileType * call s:Autocmd('FileType', expand(''), +expand('')) autocmd InsertCharPre * call s:HandleCharInsert(v:char, bufnr('%')) autocmd TextChangedP * call s:HandleTextChangedI('TextChangedP', +expand('')) autocmd TextChangedI * call s:HandleTextChangedI('TextChangedI', +expand('')) autocmd TextChanged * call s:HandleTextChanged(+expand('')) autocmd InsertLeave * call s:HandleInsertLeave(+expand('')) autocmd BufEnter * call s:HandleBufEnter(+expand('')) autocmd InsertEnter * call s:Autocmd('InsertEnter', +expand('')) autocmd BufHidden * call s:Autocmd('BufHidden', +expand('')) autocmd BufWritePost * call s:Autocmd('BufWritePost', +expand(''), getbufvar(+expand(''), 'changedtick')) autocmd CursorMoved * call s:Autocmd('CursorMoved', +expand(''), [line('.'), col('.')]) autocmd CursorMovedI * call s:Autocmd('CursorMovedI', +expand(''), [line('.'), col('.')]) autocmd CursorHold * call s:Autocmd('CursorHold', +expand(''), [line('.'), col('.')], win_getid()) autocmd CursorHoldI * call s:Autocmd('CursorHoldI', +expand(''), [line('.'), col('.')], win_getid()) autocmd BufNewFile,BufReadPost * call s:Autocmd('BufCreate', +expand('')) autocmd BufUnload * call s:Autocmd('BufUnload', +expand('')) autocmd BufWritePre * call s:SyncAutocmd('BufWritePre', +expand(''), bufname(+expand('')), getbufvar(+expand(''), 'changedtick')) autocmd FocusGained * if mode() !~# '^c' | call s:Autocmd('FocusGained') | endif autocmd FocusLost * call s:Autocmd('FocusLost') autocmd VimResized * call s:Autocmd('VimResized', &columns, &lines) autocmd VimLeavePre * call s:VimLeavePre() autocmd BufReadCmd,FileReadCmd,SourceCmd list://* call coc#list#setup(expand('')) autocmd BufWriteCmd __coc_refactor__* :call coc#rpc#notify('saveRefactor', [+expand('')]) autocmd ColorScheme * call s:Highlight() | call s:Autocmd('ColorScheme') augroup end if a:initialize == 0 call coc#rpc#request('attach', []) echohl MoreMsg echom '[coc.nvim] Event enabled' echohl None endif endfunction function! s:StaticHighlight() abort hi default CocSelectedText ctermfg=Red guifg=#fb4934 guibg=NONE hi default CocCodeLens ctermfg=Gray guifg=#999999 guibg=NONE hi default CocUnderline term=underline cterm=underline gui=underline guisp=#ebdbb2 hi default CocBold term=bold cterm=bold gui=bold hi default CocItalic term=italic cterm=italic gui=italic hi default CocStrikeThrough term=strikethrough cterm=strikethrough gui=strikethrough hi default CocMarkdownLink ctermfg=Blue guifg=#15aabf guibg=NONE hi default CocDisabled guifg=#999999 ctermfg=gray hi default CocSearch ctermfg=Blue guifg=#15aabf guibg=NONE hi default CocLink term=underline cterm=underline gui=underline guisp=#15aabf hi default link CocFloatActive CocSearch hi default link CocFadeOut Conceal hi default link CocMarkdownCode markdownCode hi default link CocMarkdownHeader markdownH1 hi default link CocDeprecatedHighlight CocStrikeThrough hi default link CocUnusedHighlight CocFadeOut hi default link CocListSearch CocSearch hi default link CocListMode ModeMsg hi default link CocListPath Comment hi default link CocHighlightText CursorColumn hi default link CocHoverRange Search hi default link CocCursorRange Search hi default link CocLinkedEditing CocCursorRange hi default link CocHighlightRead CocHighlightText hi default link CocHighlightWrite CocHighlightText " Notification hi default CocNotificationProgress ctermfg=Blue guifg=#15aabf guibg=NONE hi default link CocNotificationButton CocUnderline hi default link CocNotificationKey Comment hi default link CocNotificationError CocErrorFloat hi default link CocNotificationWarning CocWarningFloat hi default link CocNotificationInfo CocInfoFloat " Snippet hi default link CocSnippetVisual Visual " Tree view highlights hi default link CocTreeTitle Title hi default link CocTreeDescription Comment hi default link CocTreeOpenClose CocBold hi default link CocTreeSelected CursorLine hi default link CocSelectedRange CocHighlightText " Symbol highlights hi default link CocSymbolDefault MoreMsg "Pum hi default link CocPumSearch CocSearch hi default link CocPumDetail Comment hi default link CocPumMenu CocFloating hi default link CocPumShortcut Comment hi default link CocPumDeprecated CocStrikeThrough hi default CocVirtualText ctermfg=12 guifg=#504945 hi default link CocPumVirtualText CocVirtualText hi default link CocInputBoxVirtualText CocVirtualText hi default link CocFloatDividingLine CocVirtualText if &t_Co == 256 hi def CocInlineVirtualText guifg=#808080 ctermfg=244 else hi def CocInlineVirtualText guifg=#808080 ctermfg=12 endif hi def link CocInlineAnnotation MoreMsg endfunction call s:StaticHighlight() call s:AddAnsiGroups() function! s:Highlight() abort let normalFloat = s:is_vim ? 'Pmenu' : 'NormalFloat' if coc#hlgroup#get_contrast('Normal', normalFloat) > 2.0 exe 'hi default CocFloating '.coc#hlgroup#create_bg_command('Normal', &background ==# 'dark' ? -30 : 30) exe 'hi default CocMenuSel '.coc#hlgroup#create_bg_command('CocFloating', &background ==# 'dark' ? -20 : 20) exe 'hi default CocFloatThumb '.coc#hlgroup#create_bg_command('CocFloating', &background ==# 'dark' ? -40 : 40) hi default link CocFloatSbar CocFloating else exe 'hi default link CocFloating '.normalFloat if coc#hlgroup#get_contrast('CocFloating', 'PmenuSel') > 2.0 exe 'hi default CocMenuSel '.coc#hlgroup#create_bg_command('CocFloating', &background ==# 'dark' ? -30 : 30) else exe 'hi default CocMenuSel '.coc#hlgroup#get_hl_command(synIDtrans(hlID('PmenuSel')), 'bg', '237', '#13354A') endif hi default link CocFloatThumb PmenuThumb hi default link CocFloatSbar PmenuSbar endif exe 'hi default link CocFloatBorder ' .. (hlexists('FloatBorder') ? 'FloatBorder' : 'CocFloating') if coc#hlgroup#get_contrast('Normal', 'CursorLine') < 1.3 " Avoid color too close exe 'hi default CocListLine '.coc#hlgroup#create_bg_command('Normal', &background ==# 'dark' ? -20 : 20) else hi default link CocListLine CursorLine endif if !s:is_vim hi default CocCursorTransparent gui=strikethrough blend=100 endif let sign_colors = { \ 'Error': ['Red', '#ff0000'], \ 'Warn': ['Brown', '#ff922b'], \ 'Info': ['Yellow', '#fab005'], \ 'Hint': ['Blue', '#15aabf'] \ } for name in ['Error', 'Warning', 'Info', 'Hint'] let suffix = name ==# 'Warning' ? 'Warn' : name if hlexists('DiagnosticUnderline'.suffix) exe 'hi default link Coc'.name.'Highlight DiagnosticUnderline'.suffix else exe 'hi default link Coc'.name.'Highlight CocUnderline' endif if hlexists('DiagnosticSign'.suffix) exe 'hi default link Coc'.name.'Sign DiagnosticSign'.suffix else exe 'hi default Coc'.name.'Sign ctermfg='.sign_colors[suffix][0].' guifg='.sign_colors[suffix][1] endif if hlexists('DiagnosticVirtualText'.suffix) exe 'hi default link Coc'.name.'VirtualText DiagnosticVirtualText'.suffix else call s:CreateHighlight('Coc'.name.'VirtualText', 'Coc'.name.'Sign', 'Normal') endif if hlexists('Diagnostic'.suffix) exe 'hi default link Coc'.name.'Float Diagnostic'.suffix else call s:CreateHighlight('Coc'.name.'Float', 'Coc'.name.'Sign', 'CocFloating') endif endfor call s:CreateHighlight('CocInlayHint', 'CocHintSign', 'SignColumn') for name in ['Parameter', 'Type'] exe 'hi default link CocInlayHint'.name.' CocInlayHint' endfor if get(g:, 'coc_default_semantic_highlight_groups', 1) let hlMap = { \ 'TypeNamespace': ['@module', 'Include'], \ 'TypeType': ['@type', 'Type'], \ 'TypeClass': ['@constructor', 'Special'], \ 'TypeEnum': ['@type', 'Type'], \ 'TypeInterface': ['@type', 'Type'], \ 'TypeStruct': ['@structure', 'Identifier'], \ 'TypeTypeParameter': ['@variable.parameter', 'Identifier'], \ 'TypeParameter': ['@variable.parameter', 'Identifier'], \ 'TypeVariable': ['@variable', 'Identifier'], \ 'TypeProperty': ['@property', 'Identifier'], \ 'TypeEnumMember': ['@property', 'Constant'], \ 'TypeEvent': ['@keyword', 'Keyword'], \ 'TypeFunction': ['@function', 'Function'], \ 'TypeMethod': ['@function.method', 'Function'], \ 'TypeMacro': ['@constant.macro', 'Define'], \ 'TypeKeyword': ['@keyword', 'Keyword'], \ 'TypeModifier': ['@keyword.storage', 'StorageClass'], \ 'TypeComment': ['@comment', 'Comment'], \ 'TypeString': ['@string', 'String'], \ 'TypeNumber': ['@number', 'Number'], \ 'TypeBoolean': ['@boolean', 'Boolean'], \ 'TypeRegexp': ['@string.regexp', 'String'], \ 'TypeOperator': ['@operator', 'Operator'], \ 'TypeDecorator': ['@string.special.symbol', 'Identifier'], \ 'ModDeprecated': ['@markup.strikethrough', 'CocDeprecatedHighlight'] \ } for [key, value] in items(hlMap) let ts = get(value, 0, '') let fallback = get(value, 1, '') execute 'hi default link CocSem'.key.' '.(coc#hlgroup#valid(ts) ? ts : fallback) endfor endif let symbolMap = { \ 'Keyword': ['@keyword', 'Keyword'], \ 'Namespace': ['@module', 'Include'], \ 'Class': ['@constructor', 'Special'], \ 'Method': ['@method', 'Function'], \ 'Property': ['@property', 'Identifier'], \ 'Text': ['@text', 'CocSymbolDefault'], \ 'Unit': ['@unit', 'CocSymbolDefault'], \ 'Value': ['@value', 'CocSymbolDefault'], \ 'Snippet': ['@snippet', 'CocSymbolDefault'], \ 'Color': ['@color', 'Float'], \ 'Reference': ['@text.reference', 'Constant'], \ 'Folder': ['@folder', 'CocSymbolDefault'], \ 'File': ['@file', 'Statement'], \ 'Module': ['@module', 'Statement'], \ 'Package': ['@package', 'Statement'], \ 'Field': ['@variable.member', 'Identifier'], \ 'Constructor': ['@constructor', 'Special'], \ 'Enum': ['@type', 'CocSymbolDefault'], \ 'Interface': ['@type', 'CocSymbolDefault'], \ 'Function': ['@function', 'Function'], \ 'Variable': ['@variable.builtin', 'Special'], \ 'Constant': ['@constant', 'Constant'], \ 'String': ['@string', 'String'], \ 'Number': ['@number', 'Number'], \ 'Boolean': ['@boolean', 'Boolean'], \ 'Array': ['@array', 'CocSymbolDefault'], \ 'Object': ['@object', 'CocSymbolDefault'], \ 'Key': ['@key', 'Identifier'], \ 'Null': ['@null', 'Type'], \ 'EnumMember': ['@property', 'Identifier'], \ 'Struct': ['@structure', 'Keyword'], \ 'Event': ['@constant', 'Constant'], \ 'Operator': ['@operator', 'Operator'], \ 'TypeParameter': ['@variable.parameter', 'Identifier'], \ } for [key, value] in items(symbolMap) let hlGroup = coc#hlgroup#valid(value[0]) ? value[0] : get(value, 1, 'CocSymbolDefault') if hlexists(hlGroup) execute 'hi default CocSymbol'.key.' '.coc#hlgroup#get_hl_command(synIDtrans(hlID(hlGroup)), 'fg', '223', '#ebdbb2') endif endfor endfunction function! s:ShowInfo() if coc#rpc#ready() call coc#rpc#notify('showInfo', []) else let lines = [] echomsg 'coc.nvim service not started, checking environment...' let node = get(g:, 'coc_node_path', $COC_NODE_PATH == '' ? 'node' : $COC_NODE_PATH) if !executable(node) call add(lines, 'Error: '.node.' is not executable!') else let output = trim(system(node . ' --version')) let ms = matchlist(output, 'v\(\d\+\).\(\d\+\).\(\d\+\)') if empty(ms) || str2nr(ms[1]) < 16 || (str2nr(ms[1]) == 16 && str2nr(ms[2]) < 18) call add(lines, 'Error: Node version '.output.' < 16.18.0, please upgrade node.js') endif endif " check bundle let file = s:root.'/build/index.js' if !filereadable(file) call add(lines, 'Error: javascript bundle not found, please compile code of coc.nvim by esbuild.') endif if !empty(lines) botright vnew setl filetype=nofile call setline(1, lines) else if get(g:, 'coc_start_at_startup',1) echohl MoreMsg | echon 'Service stopped for some unknown reason, try :CocStart' | echohl None else echohl MoreMsg | echon 'Start on startup is disabled, try :CocStart' | echohl None endif endif endif endfunction function! s:CursorRangeFromSelected(type, ...) abort " add range by operator call coc#rpc#request('cursorsSelect', [bufnr('%'), 'operator', a:type]) endfunction function! s:FormatFromSelected(type) call CocActionAsync('formatSelected', a:type) endfunction function! s:CodeActionFromSelected(type) call CocActionAsync('codeAction', a:type) endfunction function! s:CodeActionRefactorFromSelected(type) call CocActionAsync('codeAction', a:type, ['refactor'] ,v:true) endfunction command! -nargs=0 CocOutline :call coc#rpc#notify('showOutline', []) command! -nargs=? CocDiagnostics :call s:OpenDiagnostics() command! -nargs=0 CocInfo :call s:ShowInfo() command! -nargs=0 CocOpenLog :call coc#rpc#notify('openLog', []) command! -nargs=0 CocDisable :call s:Disable() command! -nargs=0 CocEnable :call s:Enable(0) command! -nargs=0 CocConfig :call s:OpenConfig() command! -nargs=0 CocLocalConfig :call coc#rpc#notify('openLocalConfig', []) command! -nargs=0 CocRestart :call coc#rpc#restart() command! -nargs=0 CocStart :call coc#rpc#start_server() command! -nargs=0 CocPrintErrors :call coc#rpc#show_errors() command! -nargs=1 -complete=custom,s:LoadedExtensions CocWatch :call coc#rpc#notify('watchExtension', []) command! -nargs=+ -complete=custom,s:SearchOptions CocSearch :call coc#rpc#notify('search', []) command! -nargs=+ -complete=custom,s:ExtensionList CocUninstall :call CocActionAsync('uninstallExtension', ) command! -nargs=* -complete=custom,s:CommandList -range CocCommand :call coc#rpc#notify('runCommand', []) command! -nargs=* -complete=custom,coc#list#options CocList :call coc#rpc#notify('openList', []) command! -nargs=? -complete=custom,coc#list#names CocListResume :call coc#rpc#notify('listResume', []) command! -nargs=? -complete=custom,coc#list#names CocListCancel :call coc#rpc#notify('listCancel', []) command! -nargs=? -complete=custom,coc#list#names CocPrev :call coc#rpc#notify('listPrev', []) command! -nargs=? -complete=custom,coc#list#names CocNext :call coc#rpc#notify('listNext', []) command! -nargs=? -complete=custom,coc#list#names CocFirst :call coc#rpc#notify('listFirst', []) command! -nargs=? -complete=custom,coc#list#names CocLast :call coc#rpc#notify('listLast', []) command! -nargs=0 CocUpdate :call coc#util#update_extensions(1) command! -nargs=0 -bar CocUpdateSync :call coc#util#update_extensions() command! -nargs=* -bar -complete=custom,s:InstallOptions CocInstall :call coc#util#install_extension([]) call s:Enable(1) augroup coc_dynamic_autocmd autocmd! augroup END augroup coc_dynamic_content autocmd! augroup END augroup coc_dynamic_option autocmd! augroup END " Default key-mappings for completion if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#next(1) : coc#inline#visible() ? coc#inline#next() : "\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#prev(1) : coc#inline#visible() ? coc#inline#prev() : "\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#next(0) : coc#inline#visible() ? coc#inline#next() :"\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#prev(0) : coc#inline#visible() ? coc#inline#prev() :"\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#cancel() : coc#inline#visible() ? coc#inline#cancel() : "\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#confirm() : coc#inline#visible() ? coc#inline#accept() :"\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#scroll(1) : "\" endif if empty(mapcheck('', 'i')) inoremap coc#pum#visible() ? coc#pum#scroll(0) : "\" endif vnoremap (coc-range-select) :call CocActionAsync('rangeSelect', visualmode(), v:true) vnoremap (coc-range-select-backward) :call CocActionAsync('rangeSelect', visualmode(), v:false) nnoremap (coc-range-select) :call CocActionAsync('rangeSelect', '', v:true) nnoremap (coc-codelens-action) :call CocActionAsync('codeLensAction') vnoremap (coc-format-selected) :call CocActionAsync('formatSelected', visualmode()) vnoremap (coc-codeaction-selected) :call CocActionAsync('codeAction', visualmode()) vnoremap (coc-codeaction-refactor-selected) :call CocActionAsync('codeAction', visualmode(), ['refactor'], v:true) nnoremap (coc-codeaction-selected) :set operatorfunc=CodeActionFromSelectedg@ nnoremap (coc-codeaction-refactor-selected) :set operatorfunc=CodeActionRefactorFromSelectedg@ nnoremap (coc-codeaction) :call CocActionAsync('codeAction', '') nnoremap (coc-codeaction-line) :call CocActionAsync('codeAction', 'currline') nnoremap (coc-codeaction-cursor) :call CocActionAsync('codeAction', 'cursor') nnoremap (coc-codeaction-refactor) :call CocActionAsync('codeAction', 'cursor', ['refactor'], v:true) nnoremap (coc-codeaction-source) :call CocActionAsync('codeAction', '', ['source'], v:true) nnoremap (coc-rename) :call CocActionAsync('rename') nnoremap (coc-format-selected) :set operatorfunc=FormatFromSelectedg@ nnoremap (coc-format) :call CocActionAsync('format') nnoremap (coc-diagnostic-info) :call CocActionAsync('diagnosticInfo') nnoremap (coc-diagnostic-next) :call CocActionAsync('diagnosticNext') nnoremap (coc-diagnostic-prev) :call CocActionAsync('diagnosticPrevious') nnoremap (coc-diagnostic-next-error) :call CocActionAsync('diagnosticNext', 'error') nnoremap (coc-diagnostic-prev-error) :call CocActionAsync('diagnosticPrevious', 'error') nnoremap (coc-definition) :call CocActionAsync('jumpDefinition') nnoremap (coc-declaration) :call CocActionAsync('jumpDeclaration') nnoremap (coc-implementation) :call CocActionAsync('jumpImplementation') nnoremap (coc-type-definition) :call CocActionAsync('jumpTypeDefinition') nnoremap (coc-references) :call CocActionAsync('jumpReferences') nnoremap (coc-references-used) :call CocActionAsync('jumpUsed') nnoremap (coc-openlink) :call CocActionAsync('openLink') nnoremap (coc-fix-current) :call CocActionAsync('doQuickfix') nnoremap (coc-float-hide) :call coc#float#close_all() nnoremap (coc-float-jump) :call coc#float#jump() nnoremap (coc-command-repeat) :call CocAction('repeatCommand') nnoremap (coc-refactor) :call CocActionAsync('refactor') nnoremap (coc-cursors-operator) :set operatorfunc=CursorRangeFromSelectedg@ vnoremap (coc-cursors-range) :call CocAction('cursorsSelect', bufnr('%'), 'range', visualmode()) nnoremap (coc-cursors-word) :call CocAction('cursorsSelect', bufnr('%'), 'word', 'n') nnoremap (coc-cursors-position) :call CocAction('cursorsSelect', bufnr('%'), 'position', 'n') vnoremap (coc-funcobj-i) :call CocAction('selectSymbolRange', v:true, visualmode(), ['Method', 'Function']) vnoremap (coc-funcobj-a) :call CocAction('selectSymbolRange', v:false, visualmode(), ['Method', 'Function']) onoremap (coc-funcobj-i) :call CocAction('selectSymbolRange', v:true, '', ['Method', 'Function']) onoremap (coc-funcobj-a) :call CocAction('selectSymbolRange', v:false, '', ['Method', 'Function']) vnoremap (coc-classobj-i) :call CocAction('selectSymbolRange', v:true, visualmode(), ['Interface', 'Struct', 'Class']) vnoremap (coc-classobj-a) :call CocAction('selectSymbolRange', v:false, visualmode(), ['Interface', 'Struct', 'Class']) onoremap (coc-classobj-i) :call CocAction('selectSymbolRange', v:true, '', ['Interface', 'Struct', 'Class']) onoremap (coc-classobj-a) :call CocAction('selectSymbolRange', v:false, '', ['Interface', 'Struct', 'Class']) ================================================ FILE: release.sh ================================================ #!/bin/sh set -e [ "$TRACE" ] && set -x git checkout master npm run lint:typecheck if [ $? -ne 0 ]; then echo "tsc 类型检查未通过" exit 1 fi npm run lint if [ $? -ne 0 ]; then echo "eslint 检查未通过" exit 1 fi npm test if [ $? -eq 0 ]; then git config --global user.name "chemzqm" git config --global user.email "chemzqm@users.noreply.github.com" git fetch origin release --depth=1 commitmsg=$(git log --oneline -1) mkdir -p .release cp -r .github bin lua build autoload plugin history.md README.md doc .release git checkout release cp -r .release/* . nvim -c 'helptags doc|q' changes=$(git status --porcelain) if [ -n "$changes" ]; then git add . git commit -m "$commitmsg" git push origin release else echo "No need to commit." fi fi ================================================ FILE: src/__tests__/autoload/coc/source/email.vim ================================================ " vim source for emails function! coc#source#email#init() abort return { \ 'priority': 9, \ 'shortcut': 'Email', \ 'triggerCharacters': ['@'] \} endfunction function! coc#source#email#should_complete(opt) abort return 1 endfunction function! coc#source#email#complete(opt, cb) abort let items = ['foo@gmail.com', 'bar@yahoo.com'] call a:cb(items) endfunction ================================================ FILE: src/__tests__/autoload/coc/source/vim9.vim ================================================ vim9script export def Init(): dict return { priority: 9, shortcut: 'Email', triggerCharacters: ['@'] } enddef export def Complete(option: dict, Callback: func(list)) const items = ['foo@gmail.com', 'bar@yahoo.com'] Callback(items) enddef ================================================ FILE: src/__tests__/autoload/legacy.vim ================================================ function legacy#dict_add() dict return self.key + 1 endfunction function legacy#win_execute() abort call win_execute(win_getid(), ['let w:foo = "a"."b"']) endfunction ================================================ FILE: src/__tests__/autoload/vim9.vim ================================================ vim9script scriptencoding utf-8 export def WinExecute(): any win_execute(win_getid(), 'cursor(1, 1)') return v:true enddef export def Execute(cmd: string): void execute(cmd) enddef defcompile ================================================ FILE: src/__tests__/client/configuration.test.ts ================================================ import path from 'path' import { DidChangeConfigurationNotification, DocumentSelector } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { SyncConfigurationFeature } from '../../language-client/configuration' import { LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from '../../language-client/index' import workspace from '../../workspace' import helper from '../helper' function createClient(section: string | string[] | undefined, middleware: Middleware = {}, opts: Partial = {}): LanguageClient { const serverModule = path.join(__dirname, './server/configServer.js') const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } } const documentSelector: DocumentSelector = [{ scheme: 'file' }] const clientOptions: LanguageClientOptions = Object.assign({ documentSelector, synchronize: { configurationSection: section }, initializationOptions: {}, middleware }, opts) const result = new LanguageClient('test', 'Test Language Server', serverOptions, clientOptions) return result } beforeAll(async () => { await helper.setup() }) afterAll(async () => { await helper.shutdown() }) describe('pull configuration feature', () => { let client: LanguageClient beforeAll(async () => { client = createClient(undefined) await client.start() }) afterAll(async () => { await client.stop() }) it('should request all configuration', async () => { let config: any client.middleware.workspace = client.middleware.workspace ?? {} client.middleware.workspace.configuration = (params, token, next) => { config = next(params, token) return config } await client.sendNotification('pull0') await helper.waitValue(() => { return config != null }, true) expect(config[0].http).toBeDefined() }) it('should request configurations with sections', async () => { let config: any client.middleware.workspace = client.middleware.workspace ?? {} client.middleware.workspace.configuration = (params, token, next) => { config = next(params, token) return config } await client.sendNotification('pull1') await helper.waitValue(() => { return config?.length }, 3) expect(config[1]).toBeNull() expect(config[0].proxy).toBeDefined() expect(config[2]).toBeNull() }) }) describe('publish configuration feature', () => { it('should send configuration for languageserver', async () => { let client: LanguageClient client = createClient('languageserver.cpp.settings') let changed client.onNotification('configurationChange', params => { changed = params }) await client.start() await helper.waitValue(() => { return changed != null }, true) expect(changed).toEqual({ settings: {} }) await client.stop() }) it('should get configuration from workspace folder', async () => { let folder = path.resolve(__dirname, '../sample') workspace.workspaceFolderControl.addWorkspaceFolder(folder, false) let configFilePath = path.join(folder, '.vim/coc-settings.json') workspace.configurations.addFolderFile(configFilePath, false, folder) let client = createClient('coc.preferences', {}, { workspaceFolder: { name: 'sample', uri: URI.file(folder).toString() } }) let changed client.onNotification('configurationChange', params => { changed = params }) await client.start() await helper.waitValue(() => { return changed != null }, true) expect(changed.settings.coc.preferences.rootPath).toBe('./src') workspace.workspaceFolderControl.removeWorkspaceFolder(folder) let feature = client.getFeature(DidChangeConfigurationNotification.method) feature.dispose() await client.stop() }) it('should send configuration for specific sections', async () => { let client: LanguageClient let called = false client = createClient(['coc.preferences', 'npm', 'unknown'], { workspace: { didChangeConfiguration: (sections, next) => { called = true return next(sections) } } }) let changed client.onNotification('configurationChange', params => { changed = params }) await client.start() await helper.waitValue(() => { return called }, true) await helper.waitValue(() => { return changed != null }, true) expect(changed.settings.coc).toBeDefined() expect(changed.settings.npm).toBeDefined() let { configurations } = workspace configurations.updateMemoryConfig({ 'npm.binPath': 'cnpm' }) await helper.waitValue(() => { return changed.settings.npm?.binPath }, 'cnpm') await client.stop() }) it('should catch reject error', async () => { let client: LanguageClient let called = false client = createClient(['cpp'], { workspace: { didChangeConfiguration: () => { return Promise.reject(new Error('custom error')) } } }) let changed client.onNotification('configurationChange', params => { changed = params }) await client.start() await helper.wait(50) expect(called).toBe(false) void client.stop() await client.stop() }) it('should send null settings', async () => { let client: LanguageClient client = createClient(['cpp'], { workspace: { didChangeConfiguration: (_sections, next) => { return next(null) } } }) let changed client.onNotification('configurationChange', params => { changed = params }) await client.start() await helper.waitValue(() => changed != null, true) expect(changed).toEqual({ settings: null }) await client.stop() }) it('should extractSettingsInformation', async () => { let res = SyncConfigurationFeature.extractSettingsInformation(['http.proxy', 'http.proxyCA']) expect(res.http).toBeDefined() expect(res.http.proxy).toBeDefined() }) }) ================================================ FILE: src/__tests__/client/connection.test.ts ================================================ import { Duplex } from 'stream' import { createProtocolConnection, ProgressType, DocumentSymbolParams, DocumentSymbolRequest, InitializeParams, InitializeRequest, InitializeResult, ProtocolConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-languageserver-protocol/node' import { SymbolInformation, SymbolKind } from 'vscode-languageserver-types' import { NullLogger } from '../../language-client/client' class TestStream extends Duplex { public _write(chunk: string, _encoding: string, done: () => void): void { this.emit('data', chunk) done() } public _read(_size: number): void { } } let serverConnection: ProtocolConnection let clientConnection: ProtocolConnection let progressType: ProgressType = new ProgressType() beforeEach(() => { const up = new TestStream() const down = new TestStream() const logger = new NullLogger() serverConnection = createProtocolConnection(new StreamMessageReader(up), new StreamMessageWriter(down), logger) clientConnection = createProtocolConnection(new StreamMessageReader(down), new StreamMessageWriter(up), logger) serverConnection.listen() clientConnection.listen() }) afterEach(() => { serverConnection.dispose() clientConnection.dispose() }) describe('Connection Tests', () => { it('should ensure proper param passing', async () => { let paramsCorrect = false serverConnection.onRequest(InitializeRequest.type, params => { paramsCorrect = !Array.isArray(params) let result: InitializeResult = { capabilities: { } } return result }) const init: InitializeParams = { rootUri: 'file:///home/dirkb', processId: 1, capabilities: {}, workspaceFolders: null, } await clientConnection.sendRequest(InitializeRequest.type, init) expect(paramsCorrect).toBe(true) }) it('should provide token', async () => { serverConnection.onRequest(DocumentSymbolRequest.type, params => { expect(params.partialResultToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') return [] }) const params: DocumentSymbolParams = { textDocument: { uri: 'file:///abc.txt' }, partialResultToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' } await clientConnection.sendRequest(DocumentSymbolRequest.type, params) }) it('should report result', async () => { let result: SymbolInformation = { name: 'abc', kind: SymbolKind.Class, location: { uri: 'file:///abc.txt', range: { start: { line: 0, character: 1 }, end: { line: 2, character: 3 } } } } serverConnection.onRequest(DocumentSymbolRequest.type, async params => { expect(params.partialResultToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') await serverConnection.sendProgress(progressType, params.partialResultToken, [result]) return [] }) const params: DocumentSymbolParams = { textDocument: { uri: 'file:///abc.txt' }, partialResultToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' } let progressOK = false clientConnection.onProgress(progressType, '3b1db4c9-e011-489e-a9d1-0653e64707c2', values => { progressOK = (values !== undefined && values.length === 1) }) await clientConnection.sendRequest(DocumentSymbolRequest.type, params) expect(progressOK).toBeTruthy() }) it('should provide workDoneToken', async () => { serverConnection.onRequest(DocumentSymbolRequest.type, params => { expect(params.workDoneToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') return [] }) const params: DocumentSymbolParams = { textDocument: { uri: 'file:///abc.txt' }, workDoneToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' } await clientConnection.sendRequest(DocumentSymbolRequest.type, params) }) it('should report work done progress', async () => { serverConnection.onRequest(DocumentSymbolRequest.type, async params => { expect(params.workDoneToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') await serverConnection.sendProgress(progressType, params.workDoneToken, { kind: 'begin', title: 'progress' }) await serverConnection.sendProgress(progressType, params.workDoneToken, { kind: 'report', message: 'message' }) await serverConnection.sendProgress(progressType, params.workDoneToken, { kind: 'end', message: 'message' }) return [] }) const params: DocumentSymbolParams = { textDocument: { uri: 'file:///abc.txt' }, workDoneToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' } let result = '' clientConnection.onProgress(progressType, '3b1db4c9-e011-489e-a9d1-0653e64707c2', value => { result += value.kind }) await clientConnection.sendRequest(DocumentSymbolRequest.type, params) expect(result).toBe('beginreportend') }) }) ================================================ FILE: src/__tests__/client/converter.test.ts ================================================ import { CompletionTriggerKind, Position, TextDocumentItem, TextDocumentSaveReason } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import * as c2p from '../../language-client/utils/codeConverter' describe('converter', () => { function createDocument(): TextDocument { return TextDocument.create('file:///1', 'css', 1, '') } it('should convertToTextDocumentItem', () => { const cv = c2p.createConverter() let doc = createDocument() expect(cv.asTextDocumentItem(doc).uri).toBe(doc.uri) expect(TextDocumentItem.is(cv.asTextDocumentItem(doc))).toBe(true) }) it('should asCloseTextDocumentParams', () => { const cv = c2p.createConverter() let doc = createDocument() expect(cv.asCloseTextDocumentParams(doc).textDocument.uri).toBe(doc.uri) }) it('should asChangeTextDocumentParams', () => { let doc = createDocument() const cv = c2p.createConverter() expect(cv.asFullChangeTextDocumentParams(doc).textDocument.uri).toBe(doc.uri) }) it('should asWillSaveTextDocumentParams', () => { const cv = c2p.createConverter() let res = cv.asWillSaveTextDocumentParams({ document: createDocument(), bufnr: 1, reason: TextDocumentSaveReason.Manual, waitUntil: () => {} }) expect(res.textDocument).toBeDefined() expect(res.reason).toBeDefined() }) it('should asVersionedTextDocumentIdentifier', () => { const cv = c2p.createConverter() let res = cv.asVersionedTextDocumentIdentifier(createDocument()) expect(res.uri).toBeDefined() expect(res.version).toBeDefined() }) it('should asSaveTextDocumentParams', () => { const cv = c2p.createConverter() let res = cv.asSaveTextDocumentParams(createDocument(), true) expect(res.textDocument.uri).toBeDefined() expect(res.text).toBeDefined() res = cv.asSaveTextDocumentParams(createDocument()) expect(res.text).toBeUndefined() }) it('should asUri', () => { const cv = c2p.createConverter() let uri = URI.file('/tmp/a') expect(cv.asUri(uri)).toBe(uri.toString()) }) it('should asCompletionParams', () => { const cv = c2p.createConverter() let params = cv.asCompletionParams(createDocument(), Position.create(0, 0), { triggerKind: CompletionTriggerKind.Invoked }) expect(params.textDocument).toBeDefined() expect(params.position).toBeDefined() expect(params.context).toBeDefined() }) it('should asTextDocumentPositionParams', () => { const cv = c2p.createConverter() let params = cv.asTextDocumentPositionParams(createDocument(), Position.create(0, 0)) expect(params.textDocument).toBeDefined() expect(params.position).toBeDefined() }) it('should asTextDocumentIdentifier', () => { const cv = c2p.createConverter() let doc = cv.asTextDocumentIdentifier(createDocument()) expect(doc.uri).toBeDefined() }) it('should asReferenceParams', () => { const cv = c2p.createConverter() let params = cv.asReferenceParams(createDocument(), Position.create(0, 0), { includeDeclaration: false }) expect(params.textDocument.uri).toBeDefined() expect(params.position).toBeDefined() }) it('should asDocumentSymbolParams', () => { const cv = c2p.createConverter() let doc = cv.asDocumentSymbolParams(createDocument()) expect(doc.textDocument.uri).toBeDefined() }) it('should asCodeLensParams', () => { const cv = c2p.createConverter() let doc = cv.asCodeLensParams(createDocument()) expect(doc.textDocument.uri).toBeDefined() }) }) ================================================ FILE: src/__tests__/client/diagnostics.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { CancellationToken, DocumentDiagnosticRequest, Position, TextEdit } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import * as lsclient from '../../language-client' import { BackgroundScheduler, DocumentPullStateTracker, PullState } from '../../language-client/diagnostic' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' function getId(uri: string): number { let ms = uri.match(/\d+$/) return ms ? Number(ms[0]) : undefined } function createDocument(id: number, version = 1): TextDocument { let uri = `file:///${id}` return TextDocument.create(uri, '', version, '') } function createUri(id: number): URI { return URI.file(id.toString()) } describe('BackgroundScheduler', () => { it('should schedule documents by add', async () => { let uris: string[] = [] let client = { error: () => {} } let s = new BackgroundScheduler(client as any, { pull(document) { uris.push(document.uri) }, pullAsync(document) { uris.push(document.uri) return Promise.resolve(undefined) } } as any) s.add(createDocument(1)) s.add(createDocument(1)) s.add(createDocument(2)) s.add(createDocument(3)) s.trigger() await helper.waitValue(() => { return uris.length }, 3) let ids = uris.map(u => getId(u)) expect(ids).toEqual([1, 2, 3]) }) it('should schedule documents by remove', async () => { let uris: string[] = [] let client = { error: () => {} } let s = new BackgroundScheduler(client as any, { pull(document) { uris.push(document.uri) }, pullAsync(document) { uris.push(document.uri) return Promise.resolve(undefined) } } as any) s.add(createDocument(1)) s.add(createDocument(2)) s.remove(createDocument(2)) s.add(createDocument(3)) s.remove(createDocument(3)) s.trigger() s.trigger() await helper.waitValue(() => { return uris.length }, 1) let ids = uris.map(u => getId(u)) expect(ids).toEqual([1]) s.dispose() }) }) describe('DocumentPullStateTracker', () => { it('should track document', async () => { let tracker = new DocumentPullStateTracker() let state = tracker.track(PullState.document, createDocument(1)) let other = tracker.track(PullState.document, createDocument(1)) expect(state).toBe(other) tracker.track(PullState.workspace, createDocument(3)) let id = 'dcf06d3b-79f6-4a5e-bc8d-d3334f7b4cad' tracker.update(PullState.document, createDocument(1, 2), id) tracker.update(PullState.document, createDocument(2, 2), 'f758ae47-c94e-406e-ba41-0f3bb2fe4fc7') let curr = tracker.getResultId(PullState.document, createDocument(1, 2)) expect(curr).toBe(id) expect(tracker.getResultId(PullState.workspace, createDocument(1, 2))).toBeUndefined() tracker.unTrack(PullState.document, createDocument(2, 2)) expect(tracker.trackingDocuments()).toEqual(['file:///1']) tracker.update(PullState.workspace, createDocument(3, 2), 'fcb905e2-8edb-4239-9150-198c8175ed4a') tracker.update(PullState.workspace, createDocument(1, 2), 'fe96d175-c19f-4705-bff1-101bf83b2953') expect(tracker.tracks(PullState.workspace, createDocument(3, 1))).toBe(true) expect(tracker.tracks(PullState.document, createDocument(4, 1))).toBe(false) let res = tracker.getAllResultIds() expect(res.length).toBe(2) }) it('should track URI', async () => { let tracker = new DocumentPullStateTracker() let state = tracker.track(PullState.document, createUri(1), undefined) let other = tracker.track(PullState.document, createUri(1), undefined) expect(state).toBe(other) tracker.track(PullState.workspace, createUri(3), undefined) let id = 'dcf06d3b-79f6-4a5e-bc8d-d3334f7b4cad' tracker.update(PullState.document, createUri(1), undefined, id) tracker.update(PullState.document, createUri(2), undefined, 'f758ae47-c94e-406e-ba41-0f3bb2fe4fc7') let curr = tracker.getResultId(PullState.document, createUri(1)) expect(curr).toBe(id) tracker.unTrack(PullState.document, createUri(2)) expect(tracker.trackingDocuments()).toEqual(['file:///1']) tracker.update(PullState.workspace, createUri(3), undefined, undefined) tracker.update(PullState.workspace, createUri(1), undefined, 'fe96d175-c19f-4705-bff1-101bf83b2953') expect(tracker.tracks(PullState.workspace, createUri(3))).toBe(true) expect(tracker.tracks(PullState.document, createUri(4))).toBe(false) let res = tracker.getAllResultIds() expect(res.length).toBe(1) }) }) describe('DiagnosticFeature', () => { let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = workspace.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) async function createServer(interFileDependencies: boolean, workspaceDiagnostics = false, middleware: lsclient.Middleware = {}, fun?: (opt: lsclient.LanguageClientOptions) => void) { let clientOptions: lsclient.LanguageClientOptions = { documentSelector: [{ language: '*' }], middleware, initializationOptions: { interFileDependencies: interFileDependencies == true, workspaceDiagnostics } } if (fun) fun(clientOptions) let serverModule = path.join(__dirname, './server/diagnosticServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) await client.start() return client } function getUri(s: number | string): string { let fsPath = path.join(os.tmpdir(), s.toString()) return URI.file(fsPath).toString() } it('should work when change visible editor', async () => { let doc = await workspace.loadFile(getUri(1), 'edit') await workspace.loadFile(getUri(3), 'tabe') let client = await createServer(true) await helper.wait(30) await workspace.loadFile(getUri(2), 'edit') await helper.wait(30) await workspace.loadFile(getUri(3), 'tabe') await helper.wait(30) let feature = client.getFeature(DocumentDiagnosticRequest.method) expect(feature).toBeDefined() let provider = feature.getProvider(doc.textDocument) let res = provider.knows(PullState.document, doc.textDocument) expect(res).toBe(false) await client.stop() }) it('should filter by document selector', async () => { let client = await createServer(true, false, {}, opt => { opt.documentSelector = [{ language: 'vim' }] }) let doc = await workspace.loadFile(getUri(1), 'edit') await helper.wait(10) let feature = client.getFeature(DocumentDiagnosticRequest.method) let provider = feature.getProvider(TextDocument.create('file:///1', 'vim', 1, '')) let res = provider.knows(PullState.document, doc.textDocument) expect(res).toBe(false) doc = await workspace.loadFile(getUri(2), 'edit') await helper.waitValue(() => window.activeTextEditor?.uri === doc.uri, true) provider.forget(doc.textDocument) await client.stop() }) it('should filter by ignore', async () => { let client = await createServer(true, false, {}, opt => { opt.diagnosticPullOptions = { ignored: ['**/*.ts'] } }) let doc = await workspace.loadFile(getUri('a.ts'), 'edit') await helper.wait(10) let feature = client.getFeature(DocumentDiagnosticRequest.method) let provider = feature.getProvider(doc.textDocument) let res = provider.knows(PullState.document, doc.textDocument) expect(res).toBe(false) await client.stop() }) it('should not throw on request error', async () => { let client = await createServer(true) await workspace.loadFile(getUri('error'), 'edit') await workspace.loadFile(getUri('cancel'), 'tabe') await workspace.loadFile(getUri('retrigger'), 'tabe') await helper.wait(10) await nvim.command('normal! 2gt') await workspace.loadFile(getUri('unchanged'), 'edit') await helper.wait(20) await client.stop() }) it('should pull diagnostic on change', async () => { let doc = await workspace.loadFile(getUri('change'), 'edit') let client = await createServer(true, false, {}, opt => { opt.diagnosticPullOptions = { onChange: true, filter: doc => { return doc.uri.endsWith('filtered') } } }) let feature = client.getFeature(DocumentDiagnosticRequest.method) let provider = feature.getProvider(doc.textDocument) await helper.waitValue(() => { return provider.knows(PullState.document, doc.textDocument) }, true) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) await helper.waitValue(async () => { let n = await client.sendRequest('getChangeCount') as number return n >= 2 }, true) await nvim.call('setline', [1, 'foo']) let d = await workspace.loadFile(getUri('filtered'), 'tabe') await d.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) await helper.wait(30) feature.refresh() await nvim.command(`bd! ${doc.bufnr}`) await client.stop() }) it('should pull diagnostic on save', async () => { let doc = await workspace.loadFile(getUri(uuid() + 'filtered'), 'edit') await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) doc = await workspace.loadFile(getUri(uuid() + 'save'), 'tabe') let client = await createServer(true, false, {}, opt => { opt.diagnosticPullOptions = { onSave: true, filter: doc => { return doc.uri.endsWith('filtered') } } }) let feature = client.getFeature(DocumentDiagnosticRequest.method) let provider = feature.getProvider(doc.textDocument) await helper.waitValue(() => { return provider.knows(PullState.document, doc.textDocument) }, true) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) await nvim.command('wa') await helper.wait(10) await client.stop() }) it('should use provideDiagnostics middleware', async () => { let called = false let callHandle = false let middleware = { provideDiagnostics: (doc, id, token, next) => { called = true return next(doc, id, token) }, handleDiagnostics: (uri, diagnostics, next) => { callHandle = true return next(uri, diagnostics) } } let client = await createServer(true, false, middleware) let feature = client.getFeature(DocumentDiagnosticRequest.method) expect(feature).toBeDefined() let textDocument = TextDocument.create(getUri('empty'), 'e', 1, '') let provider = feature.getProvider(textDocument) let res = await provider.diagnostics.provideDiagnostics(textDocument, '', CancellationToken.None) expect(called).toBe(true) expect(res).toEqual({ kind: 'full', items: [] }) await helper.waitValue(() => { return callHandle }, true) middleware.handleDiagnostics = undefined await client.sendRequest('sendDiagnostics') await client.stop() }) it('should use provideWorkspaceDiagnostics middleware', async () => { let called = false let client = await createServer(false, true, { provideWorkspaceDiagnostics: (resultIds, token, resultReporter, next) => { called = true return next(resultIds, token, resultReporter) } }) expect(called).toBe(true) await helper.waitValue(async () => { let count = await client.sendRequest('getWorkspaceCount') as number return count > 1 }, true) await client.stop() }) it('should receive partial result', async () => { let client = await createServer(false, true, {}, opt => { opt.diagnosticPullOptions = { workspace: false } }) let feature = client.getFeature(DocumentDiagnosticRequest.method) let textDocument = TextDocument.create(getUri('empty'), 'e', 1, '') let provider = feature.getProvider(textDocument) let n = 0 await provider.diagnostics.provideWorkspaceDiagnostics([{ uri: 'uri', value: '1' }], CancellationToken.None, chunk => { n++ }) expect(n).toBe(4) await client.stop() }) it('should fire refresh event', async () => { let client = await createServer(true, false, {}) let feature = client.getFeature(DocumentDiagnosticRequest.method) let textDocument = TextDocument.create(getUri('1'), 'e', 1, '') let provider = feature.getProvider(textDocument) let called = false provider.onDidChangeDiagnosticsEmitter.event(() => { called = true }) await client.sendNotification('fireRefresh') await helper.waitValue(() => { return called }, true) await client.stop() }) }) ================================================ FILE: src/__tests__/client/dynamic.test.ts ================================================ import os from 'os' import path from 'path' import { CancellationToken, CodeActionRequest, CodeLensRequest, CompletionRequest, DidChangeWorkspaceFoldersNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidRenameFilesNotification, DocumentLinkRequest, DocumentSymbolRequest, ExecuteCommandRequest, InlayHintRequest, InlineValueRequest, Position, Range, RenameRequest, SemanticTokensRegistrationType, SymbolInformation, SymbolKind, TextDocumentContentRequest, WillDeleteFilesRequest, WillRenameFilesRequest, WorkspaceFolder, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import commands from '../../commands' import * as lsclient from '../../language-client' import { ClientState } from '../../language-client' import { SemanticTokensFeature } from '../../language-client/semanticTokens' import type { TextDocumentContentProviderShape } from '../../language-client/textDocumentContent' import workspace from '../../workspace' import helper from '../helper' beforeAll(async () => { await helper.setup() }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('DynamicFeature', () => { let textDocument = TextDocument.create('file:///1', 'vim', 1, '\n') let position = Position.create(1, 1) let token = CancellationToken.None async function startServer(opts: any = {}, middleware: lsclient.Middleware = {}): Promise { let clientOptions: lsclient.LanguageClientOptions = { documentSelector: [{ language: '*' }], initializationOptions: opts, synchronize: { configurationSection: 'languageserver.vim.settings' }, middleware } let serverModule = path.join(__dirname, './server/dynamicServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) await client.start() return client } describe('RenameFeature', () => { it('should start server', async () => { let called = false let client = await startServer({ prepareRename: false }, { handleRegisterCapability: async (params, next) => { await Promise.resolve(next(params, CancellationToken.None)) return }, handleUnregisterCapability: async (params, next) => { called = true await Promise.resolve(next(params, CancellationToken.None)) return } }) let feature = client.getFeature(RenameRequest.method) let provider = feature.getProvider(textDocument) expect(provider.prepareRename).toBeUndefined() feature.unregister('') client['_state'] = ClientState.StartFailed await helper.waitValue(() => called, true) await client.stop() }) it('should handle different result', async () => { let client = await startServer({ prepareRename: true }, { provideRenameEdits: (doc, pos, newName, token, next) => { return next(doc, pos, newName, token) }, prepareRename: (doc, pos, token, next) => { return next(doc, pos, token) } }) let feature = client.getFeature(RenameRequest.method) let provider = feature.getProvider(textDocument) expect(provider.prepareRename).toBeDefined() let res = await provider.prepareRename(textDocument, position, token) expect(res).toBeNull() await client.sendRequest('setPrepareResponse', { defaultBehavior: true }) res = await provider.prepareRename(textDocument, position, token) expect(res).toBeNull() await client.sendRequest('setPrepareResponse', { range: Range.create(0, 0, 0, 3), placeholder: 'placeholder' }) res = await provider.prepareRename(textDocument, position, token) expect((res as any).placeholder).toBe('placeholder') await expect(async () => { await client.sendRequest('setPrepareResponse', { defaultBehavior: false }) res = await provider.prepareRename(textDocument, position, token) }).rejects.toThrow(Error) await client.stop() }) }) describe('WorkspaceSymbolFeature', () => { it('should use middleware', async () => { let client = await startServer({}, { provideWorkspaceSymbols: (query, token, next) => { return next(query, token) }, resolveWorkspaceSymbol: (item, token, next) => { return next(item, token) } }) let feature = client.getFeature(WorkspaceSymbolRequest.method) await helper.waitValue(() => { return feature.getProviders().length }, 2) let provider = feature.getProviders().find(o => typeof o.resolveWorkspaceSymbol === 'function') expect(provider).toBeDefined() let token = CancellationToken.None let res = await provider.provideWorkspaceSymbols('', token) expect(res.length).toBe(0) let sym = SymbolInformation.create('name', SymbolKind.Array, Range.create(0, 1, 0, 1), 'file:///1') let resolved = await provider.resolveWorkspaceSymbol(sym, token) expect(resolved.name).toBe(sym.name) await client.stop() }) }) describe('SemanticTokensFeature', () => { it('should register semanticTokens', async () => { let client = await startServer({}) let feature = client.getFeature(SemanticTokensRegistrationType.method) let provider: any await helper.waitValue(() => { provider = feature.getProvider(textDocument) return provider != null }, true) expect(provider.range).toBeUndefined() await client.stop() }) it('should use middleware', async () => { let client = await startServer({ rangeTokens: true, delta: true }, {}) let feature = client.getFeature(SemanticTokensRegistrationType.method) await helper.waitValue(() => { return feature.getProvider(textDocument) != null }, true) let provider = feature.getProvider(textDocument) expect(provider).toBeDefined() expect(provider.range).toBeDefined() let res = await provider.full.provideDocumentSemanticTokensEdits(textDocument, '2', CancellationToken.None) expect(res.resultId).toBe('3') await client.stop() }) }) describe('CodeActionFeature', () => { it('should use registered command', async () => { let client = await startServer({}) let feature = client.getFeature(CodeActionRequest.method) await helper.waitValue(() => { return feature.getProvider(textDocument) != null }, true) let provider = feature.getProvider(textDocument) let actions = await provider.provideCodeActions(textDocument, Range.create(0, 1, 0, 1), { diagnostics: [] }, token) expect(actions.length).toBe(1) await client.stop() }) }) describe('PullConfigurationFeature', () => { it('should pull configuration for configured languageserver', async () => { helper.updateConfiguration('languageserver.vim.settings.foo', 'bar') let client = await startServer({}) await client.sendNotification('pullConfiguration') await helper.wait(50) let res = await client.sendRequest('getConfiguration') expect(Array.isArray(res)).toBe(true) expect(res[0]).toEqual('bar') helper.updateConfiguration('suggest.noselect', true) await helper.wait(20) await client.stop() }) }) describe('CodeLensFeature', () => { it('should use codeLens middleware', async () => { let fn = jest.fn() let client = await startServer({}, { provideCodeLenses: (doc, token, next) => { fn() return next(doc, token) }, resolveCodeLens: (codelens, token, next) => { fn() return next(codelens, token) } }) let feature = client.getFeature(CodeLensRequest.method) let provider = feature.getProvider(textDocument).provider expect(provider).toBeDefined() let res = await provider.provideCodeLenses(textDocument, token) expect(res.length).toBe(2) let resolved = await provider.resolveCodeLens(res[0], token) expect(resolved.command).toBeDefined() expect(fn).toHaveBeenCalledTimes(2) await client.stop() }) it('should no resolve when resolve not exists', async () => { let client = await startServer({ noResolve: true }, {}) let feature = client.getFeature(CodeLensRequest.method) let provider = feature.getProvider(textDocument).provider expect(provider).toBeDefined() expect(provider.resolveCodeLens).toBeUndefined() { let feature = client.getFeature(DocumentLinkRequest.method) let provider = feature.getProvider(textDocument) expect(provider).toBeDefined() expect(provider.resolveDocumentLink).toBeUndefined() } { let feature = client.getFeature(InlayHintRequest.method) let provider = feature.getProvider(textDocument).provider expect(provider).toBeDefined() expect(provider.resolveInlayHint).toBeUndefined() } { let feature: SemanticTokensFeature await helper.waitValue(() => { feature = client.getFeature(SemanticTokensRegistrationType.method) as SemanticTokensFeature return feature != null && feature.getProvider(textDocument) != null }, true) let provider = feature.getProvider(textDocument).full expect(provider).toBeDefined() expect(provider.provideDocumentSemanticTokensEdits).toBeUndefined() } await client.stop() }) }) describe('InlineValueFeature', () => { it('should fire refresh', async () => { let client = await startServer({}) let feature = client.getFeature(InlineValueRequest.method) expect(feature).toBeDefined() await helper.waitValue(() => { return feature.getProvider(textDocument) != null }, true) let provider = feature.getProvider(textDocument) let called = false provider.onDidChangeInlineValues.event(() => { called = true }) await client.sendNotification('fireInlineValueRefresh') await helper.waitValue(() => { return called }, true) await client.stop() }) }) describe('ExecuteCommandFeature', () => { it('should register command with middleware', async () => { let called = false let client = await startServer({}, { executeCommand: (cmd, args, next) => { called = true return next(cmd, args) } }) await helper.waitValue(() => { return commands.has('test_command') }, true) let feature = client.getFeature(ExecuteCommandRequest.method) expect(feature).toBeDefined() feature.unregister('other_command') expect(feature.getState().kind).toBe('workspace') let res = await commands.executeCommand('test_command') expect(res).toEqual({ success: true }) expect(called).toBe(true) let err let spy = jest.spyOn(client, 'handleFailedRequest').mockImplementation((_type, _token, error) => { err = error }) await commands.executeCommand('other_command') spy.mockRestore() expect(err.message).toMatch(/not exists/) await client.sendNotification('unregister') await helper.waitValue(() => { return commands.has('test_command') }, false) await client.stop() }) it('should register command without middleware', async () => { let client = await startServer({}, {}) await helper.waitValue(() => { return commands.has('test_command') }, true) let res = await commands.executeCommand('test_command') expect(res).toEqual({ success: true }) await client.stop() }) }) describe('DocumentSymbolFeature', () => { it('should provide documentSymbols without middleware', async () => { let client = await startServer({}, {}) let feature = client.getFeature(DocumentSymbolRequest.method) expect(feature).toBeDefined() expect(feature.getState()).toBeDefined() let provider = feature.getProvider(textDocument) let res = await provider.provideDocumentSymbols(textDocument, token) expect(res).toEqual([]) await client.stop() }) it('should provide documentSymbols with middleware', async () => { let called = false let client = await startServer({ label: true }, { provideDocumentSymbols: (doc, token, next) => { called = true return next(doc, token) } }) let feature = client.getFeature(DocumentSymbolRequest.method) let provider = feature.getProvider(textDocument) expect(provider.meta).toEqual({ label: 'test' }) let res = await provider.provideDocumentSymbols(textDocument, token) expect(res).toEqual([]) expect(called).toBe(true) await client.stop() }) }) describe('FileOperationFeature', () => { it('should use middleware for FileOperationFeature', async () => { let n = 0 let client = await startServer({}, { workspace: { didCreateFiles: (ev, next) => { n++ return next(ev) }, didRenameFiles: (ev, next) => { n++ return next(ev) }, didDeleteFiles: (ev, next) => { n++ return Promise.reject(new Error('my error')) }, willRenameFiles: (ev, next) => { n++ return next(ev) }, willDeleteFiles: (ev, next) => { n++ return next(ev) } } }) let createFeature = client.getFeature(DidCreateFilesNotification.method) createFeature.initialize({ workspace: { fileOperations: { didCreate: { filters: [{ pattern: { glob: '' } }] } } } }, ['*']) await createFeature.send({ files: [URI.file('/a/b')] }) let renameFeature = client.getFeature(DidRenameFilesNotification.method) await renameFeature.send({ files: [{ oldUri: URI.file('/a/b'), newUri: URI.file('/c/d') }] }) let deleteFeature = client.getFeature(DidDeleteFilesNotification.method) await deleteFeature.send({ files: [URI.file('/x/y')] }) let willRename = client.getFeature(WillRenameFilesRequest.method) await willRename.send({ files: [{ oldUri: URI.file(__dirname), newUri: URI.file(path.join(__dirname, 'x')) }], waitUntil: () => {} }) let willDelete = client.getFeature(WillDeleteFilesRequest.method) await willDelete.send({ files: [URI.file('/x/y')], waitUntil: () => {} }) await willDelete.send({ files: [], waitUntil: () => {} }) await helper.waitValue(() => { return n }, 5) await client.stop() }) it('should filter matches', async () => { let n = 0 let client = await startServer({}, { workspace: { didCreateFiles: (ev, next) => { n += ev.files.length return next(ev) } } }) let createFeature = client.getFeature(DidCreateFilesNotification.method) createFeature.initialize({ workspace: { fileOperations: { didCreate: { filters: [ { pattern: { glob: '**/', matches: 'folder' } }, { pattern: { glob: '**', matches: 'file' } }, ] } } } }, ['*']) await createFeature.send({ files: [URI.file(os.tmpdir()), URI.file(__filename)] }) await helper.waitValue(() => n, 2) await client.stop() }) }) describe('CompletionItemFeature', () => { it('should register multiple completion sources', async () => { let client = await startServer({}, {}) let feature = client.getFeature(CompletionRequest.method) await helper.waitValue(() => { return feature.registrationLength }, 2) await client.stop() }) }) describe('WorkspaceFoldersFeature', () => { it('should register listeners', async () => { let client = await startServer({}, {}) let feature = client.getFeature(DidChangeWorkspaceFoldersNotification.method) expect(feature).toBeDefined() let state = feature.getState() as any expect(state.registrations).toBe(true) feature.register({ id: '1', registerOptions: undefined }) feature.unregister('b346648e-88e0-44e3-91e3-52fd6addb8c7') feature.unregister('2') await client.stop() }) it('should handle WorkspaceFoldersRequest', async () => { let client = await startServer({ changeNotifications: true }, {}) let folders = workspace.workspaceFolders expect(folders.length).toBe(0) await client.sendNotification('requestFolders') await helper.wait(10) let res = await client.sendRequest('getFolders') expect(res).toBeNull() workspace.workspaceFolderControl.addWorkspaceFolder(process.cwd(), true) await helper.wait(10) await client.stop() }) it('should use workspaceFolders middleware', async () => { await workspace.loadFile(__filename) let folders = workspace.workspaceFolders expect(folders.length).toBe(1) let called = false let fn = jest.fn() let client = await startServer({ changeNotifications: true }, { workspace: { workspaceFolders: (token, next) => { called = true return next(token) }, didChangeWorkspaceFolders: () => { fn() return Promise.reject(new Error('my error')) } } }) await client.sendNotification('requestFolders') await helper.waitValue(async () => { let res = await client.sendRequest('getFolders') as WorkspaceFolder[] return Array.isArray(res) && res.length == 1 }, true) expect(called).toBe(true) workspace.workspaceFolderControl.addWorkspaceFolder(os.tmpdir(), true) expect(fn).toHaveBeenCalled() await client.stop() }) it('should send folders event with middleware', async () => { let called = false let client = await startServer({ changeNotifications: true }, { workspace: { didChangeWorkspaceFolders: (ev, next) => { called = true return next(ev) } } }) let folders = workspace.workspaceFolders expect(folders.length).toBe(0) await workspace.loadFile(__filename) await helper.waitValue(() => { return called }, true) await client.stop() }) }) describe('TextDocumentContentFeature', () => { it('should register static TextDocumentContent feature', async () => { let client = await startServer({ textDocumentContent: true }, {}) let feature = client.getFeature(TextDocumentContentRequest.method) expect(feature.getState()['registrations']).toBe(true) let providers = feature.getProviders() as TextDocumentContentProviderShape[] let provider = providers[0] expect(provider.scheme).toBe('lsptest') let times = 0 provider.provider.onDidChange(() => { times++ }) await client.sendNotification('fireDocumentContentRefresh') await helper.waitValue(() => times, 1) let uri = URI.parse('lsptest:///1') let spy = jest.spyOn(client, 'sendRequest').mockReturnValue(Promise.resolve(undefined)) let res = await provider.provider.provideTextDocumentContent(uri, token) expect(res).toBeUndefined() spy.mockRestore() spy = jest.spyOn(client, 'sendRequest').mockReturnValue(Promise.resolve({ text: 'foo' })) res = await provider.provider.provideTextDocumentContent(uri, token) expect(res).toBe('foo') spy.mockRestore() spy = jest.spyOn(client, 'sendRequest').mockReturnValue(Promise.reject(new Error('myerror'))) await expect(async () => { await provider.provider.provideTextDocumentContent(uri, token) }).rejects.toThrow(Error) spy.mockRestore() feature.unregister('b346648e-88e0-44e3-91e3-52fd6addb8c7') expect(feature.getState()['registrations']).toBe(false) await client.stop() }) }) }) ================================================ FILE: src/__tests__/client/features.test.ts ================================================ import * as assert from 'assert' import path from 'path' import { v4 as uuidv4 } from 'uuid' import { ApplyWorkspaceEditParams, CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CallHierarchyPrepareRequest, CancellationToken, CancellationTokenSource, CodeAction, CodeActionRequest, CodeLensRequest, Color, ColorInformation, ColorPresentation, CompletionItem, CompletionRequest, CompletionTriggerKind, ConfigurationRequest, DeclarationRequest, DefinitionRequest, DidChangeConfigurationNotification, DidChangeTextDocumentNotification, DidChangeWatchedFilesNotification, DidCloseTextDocumentNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidOpenTextDocumentNotification, DidRenameFilesNotification, DidSaveTextDocumentNotification, Disposable, DocumentColorRequest, DocumentDiagnosticReport, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentFormattingRequest, DocumentHighlight, DocumentHighlightKind, DocumentHighlightRequest, DocumentLink, DocumentLinkRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest, DocumentSelector, DocumentSymbolRequest, ErrorCodes, FoldingRange, FoldingRangeRequest, FullDocumentDiagnosticReport, Hover, HoverRequest, ImplementationRequest, InlayHintKind, InlayHintLabelPart, InlayHintRequest, InlineCompletionItem, InlineCompletionRequest, InlineValueEvaluatableExpression, InlineValueRequest, InlineValueText, InlineValueVariableLookup, LinkedEditingRangeRequest, Location, NotificationType0, ParameterInformation, Position, ProgressToken, ProtocolRequestType, Range, ReferencesRequest, RenameRequest, ResponseError, SelectionRange, SelectionRangeRequest, SemanticTokensRegistrationType, SignatureHelpRequest, SignatureHelpTriggerKind, SignatureInformation, TextDocumentContentRequest, TextDocumentEdit, TextDocumentSyncKind, TextEdit, TypeDefinitionRequest, TypeHierarchyPrepareRequest, WillCreateFilesRequest, WillDeleteFilesRequest, WillRenameFilesRequest, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest, WorkDoneProgressBegin, WorkDoneProgressCreateRequest, WorkDoneProgressEnd, WorkDoneProgressReport, WorkspaceEdit, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import commands from '../../commands' import { StaticFeature } from '../../language-client/features' import { LanguageClient, LanguageClientOptions, Middleware, RevealOutputChannelOn, ServerOptions, State, TransportKind } from '../../language-client/index' import { InlayHintsFeature } from '../../language-client/inlayHint' import languages from '../../languages' import workspace from '../../workspace' import helper from '../helper' import { FoldingRangeFeature } from '../../language-client/foldingRange' beforeAll(async () => { await helper.setup() }) afterAll(async () => { await helper.shutdown() }) describe('Client integration', () => { let client!: LanguageClient let middleware: Middleware let uri!: string let document!: TextDocument let tokenSource!: CancellationTokenSource const position: Position = Position.create(1, 1) const range: Range = Range.create(1, 1, 1, 2) let contentProviderDisposable: Disposable function rangeEqual(range: Range, sl: number, sc: number, el: number, ec: number): void { assert.strictEqual(range.start.line, sl) assert.strictEqual(range.start.character, sc) assert.strictEqual(range.end.line, el) assert.strictEqual(range.end.character, ec) } function positionEqual(pos: Position, l: number, c: number): void { assert.strictEqual(pos.line, l) assert.strictEqual(pos.character, c) } function colorEqual(color: Color, red: number, green: number, blue: number, alpha: number): void { assert.strictEqual(color.red, red) assert.strictEqual(color.green, green) assert.strictEqual(color.blue, blue) assert.strictEqual(color.alpha, alpha) } function uriEqual(actual: string, expected: string): void { assert.strictEqual(actual, expected) } function isArray(value: Array | undefined | null, clazz: any, length = 1): asserts value is Array { assert.ok(Array.isArray(value), `value is array`) assert.strictEqual(value!.length, length, 'value has given length') if (clazz && typeof clazz.is === 'function') { for (let item of value) { assert.ok(clazz.is(item)) } } } function isDefined(value: T | undefined | null): asserts value is Exclude { if (value === undefined || value === null) { throw new Error(`Value is null or undefined`) } } function isFullDocumentDiagnosticReport(value: DocumentDiagnosticReport): asserts value is FullDocumentDiagnosticReport { assert.ok(value.kind === DocumentDiagnosticReportKind.Full) } beforeAll(async () => { contentProviderDisposable = workspace.registerTextDocumentContentProvider('lsptests', { provideTextDocumentContent: (_uri: URI) => { return [ 'REM @ECHO OFF', 'cd c:\\source', 'REM This is the location of the files that you want to sort', 'FOR %%f IN (*.doc *.txt) DO XCOPY c:\\source\\"%%f" c:\\text /m /y', 'REM This moves any files with a .doc or', 'REM .txt extension from c:\\source to c:\\text', 'REM %%f is a variable', 'FOR %%f IN (*.jpg *.png *.bmp) DO XCOPY C:\\source\\"%%f" c:\\images /m /y', 'REM This moves any files with a .jpg, .png,', 'REM or .bmp extension from c:\\source to c:\\images;;', ].join('\n') } }) uri = URI.parse('lsptests://localhost/test.bat').toString() let doc = await workspace.loadFile(uri.toString()) document = doc.textDocument tokenSource = new CancellationTokenSource() const serverModule = path.join(__dirname, './server/testServer.js') const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } } const documentSelector: DocumentSelector = [{ scheme: 'lsptests' }] middleware = {} const clientOptions: LanguageClientOptions = { documentSelector, synchronize: {}, initializationOptions: {}, middleware, workspaceFolder: { name: 'test_folder', uri: URI.parse('file-test:///').toString() }, outputChannel: helper.createNullChannel(), revealOutputChannelOn: RevealOutputChannelOn.Warn } client = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions) let p = client.onReady() await client.start() await p }) afterAll(async () => { await client.sendNotification('unregister') await helper.wait(30) contentProviderDisposable.dispose() await client.stop() }) test('InitializeResult', () => { let expected = { capabilities: { textDocumentSync: TextDocumentSyncKind.Full, definitionProvider: true, hoverProvider: true, signatureHelpProvider: { triggerCharacters: [','], retriggerCharacters: [';'] }, completionProvider: { resolveProvider: true, triggerCharacters: ['"', ':'] }, referencesProvider: true, documentHighlightProvider: true, codeActionProvider: { resolveProvider: true }, codeLensProvider: { resolveProvider: true }, documentFormattingProvider: true, documentRangeFormattingProvider: { rangesSupport: true }, documentOnTypeFormattingProvider: { firstTriggerCharacter: ':' }, renameProvider: { prepareProvider: true }, documentLinkProvider: { resolveProvider: true }, documentSymbolProvider: true, colorProvider: true, declarationProvider: true, foldingRangeProvider: true, implementationProvider: { documentSelector: [{ language: '*' }] }, selectionRangeProvider: true, inlineCompletionProvider: {}, inlineValueProvider: {}, inlayHintProvider: { resolveProvider: true }, typeDefinitionProvider: { id: '82671a9a-2a69-4e9f-a8d7-e1034eaa0d2e', documentSelector: [{ language: '*' }] }, callHierarchyProvider: true, semanticTokensProvider: { legend: { tokenTypes: [], tokenModifiers: [] }, range: true, full: { delta: true } }, workspace: { fileOperations: { didCreate: { filters: [{ scheme: 'file', pattern: { glob: '**/created-static/**{/,/*.txt}' } }] }, didRename: { filters: [ { scheme: 'file', pattern: { glob: '**/renamed-static/**/', matches: 'folder' } }, { scheme: 'file', pattern: { glob: '**/renamed-static/**/*.txt', matches: 'file' } } ] }, didDelete: { filters: [{ scheme: 'file', pattern: { glob: '**/deleted-static/**{/,/*.txt}' } }] }, willCreate: { filters: [{ scheme: 'file', pattern: { glob: '**/created-static/**{/,/*.txt}' } }] }, willRename: { filters: [ { scheme: 'file', pattern: { glob: '**/renamed-static/**/', matches: 'folder' } }, { scheme: 'file', pattern: { glob: '**/renamed-static/**/*.txt', matches: 'file' } } ] }, willDelete: { filters: [{ scheme: 'file', pattern: { glob: '**/deleted-static/**{/,/*.txt}' } }] }, }, textDocumentContent: { schemes: ['content-test'] } }, linkedEditingRangeProvider: true, diagnosticProvider: { identifier: 'da348dc5-c30a-4515-9d98-31ff3be38d14', interFileDependencies: true, workspaceDiagnostics: true }, typeHierarchyProvider: true, workspaceSymbolProvider: { resolveProvider: true }, notebookDocumentSync: { notebookSelector: [{ notebook: { notebookType: 'jupyter-notebook' }, cells: [{ language: 'python' }] }] } }, customResults: { hello: 'world' } } assert.deepEqual(client.initializeResult, expected) }) test('feature.getState()', async () => { const testFeature = (method: string, kind: string): void => { let feature = client.getFeature(method as any) assert.notStrictEqual(feature, undefined) let res = feature.getState() assert.strictEqual(res.kind, kind) } const testStaticFeature = (method: string, kind: string): void => { let feature = client.getStaticFeature(method as any) assert.notStrictEqual(feature, undefined) let res = feature.getState() assert.strictEqual(res.kind, kind) assert.ok(StaticFeature.is(feature)) } testStaticFeature(ConfigurationRequest.method, 'static') testStaticFeature(WorkDoneProgressCreateRequest.method, 'window') testFeature(DidChangeWatchedFilesNotification.method, 'workspace') testFeature(DidChangeConfigurationNotification.method, 'workspace') testFeature(DidOpenTextDocumentNotification.method, 'document') testFeature(DidChangeTextDocumentNotification.method, 'document') testFeature(WillSaveTextDocumentNotification.method, 'document') testFeature(WillSaveTextDocumentWaitUntilRequest.method, 'document') testFeature(DidSaveTextDocumentNotification.method, 'document') testFeature(DidCloseTextDocumentNotification.method, 'document') testFeature(DidCreateFilesNotification.method, 'workspace') testFeature(DidRenameFilesNotification.method, 'workspace') testFeature(DidDeleteFilesNotification.method, 'workspace') testFeature(WillCreateFilesRequest.method, 'workspace') testFeature(WillRenameFilesRequest.method, 'workspace') testFeature(WillDeleteFilesRequest.method, 'workspace') testFeature(CompletionRequest.method, 'document') testFeature(HoverRequest.method, 'document') testFeature(SignatureHelpRequest.method, 'document') testFeature(DefinitionRequest.method, 'document') testFeature(ReferencesRequest.method, 'document') testFeature(DocumentHighlightRequest.method, 'document') testFeature(CodeActionRequest.method, 'document') testFeature(CodeLensRequest.method, 'document') testFeature(DocumentFormattingRequest.method, 'document') testFeature(DocumentRangeFormattingRequest.method, 'document') testFeature(DocumentOnTypeFormattingRequest.method, 'document') testFeature(RenameRequest.method, 'document') testFeature(DocumentSymbolRequest.method, 'document') testFeature(DocumentLinkRequest.method, 'document') testFeature(DocumentColorRequest.method, 'document') testFeature(DeclarationRequest.method, 'document') testFeature(FoldingRangeRequest.method, 'document') testFeature(ImplementationRequest.method, 'document') testFeature(SelectionRangeRequest.method, 'document') testFeature(TypeDefinitionRequest.method, 'document') testFeature(CallHierarchyPrepareRequest.method, 'document') testFeature(SemanticTokensRegistrationType.method, 'document') testFeature(LinkedEditingRangeRequest.method, 'document') testFeature(TypeHierarchyPrepareRequest.method, 'document') testFeature(InlineCompletionRequest.method, 'document') testFeature(InlineValueRequest.method, 'document') testFeature(InlayHintRequest.method, 'document') testFeature(WorkspaceSymbolRequest.method, 'workspace') testFeature(DocumentDiagnosticRequest.method, 'document') }) test('warn and show output', async () => { global.__showOutput = true let called = false let spy = jest.spyOn(client.outputChannel, 'show').mockImplementation(() => { called = true }) client.warn(undefined, { x: 1 }, true) await helper.waitValue(() => called, true) spy.mockRestore() }) test('Goto Definition', async () => { const provider = client.getFeature(DefinitionRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideDefinition(document, position, tokenSource.token)) as Location assert.strictEqual(Location.is(result), true) uriEqual(result.uri, uri) rangeEqual(result.range, 0, 0, 0, 1) let middlewareCalled = false middleware.provideDefinition = (document, position, token, next) => { middlewareCalled = true return next(document, position, token) } await provider.provideDefinition(document, position, tokenSource.token) middleware.provideDefinition = undefined assert.strictEqual(middlewareCalled, true) }) test('Hover', async () => { const provider = client.getFeature(HoverRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideHover(document, position, tokenSource.token) assert.ok(Hover.is(result)) assert.strictEqual((result.contents as any).kind, 'plaintext') assert.strictEqual((result.contents as any).value, 'foo') let middlewareCalled = false middleware.provideHover = (document, position, token, next) => { middlewareCalled = true return next(document, position, token) } await provider.provideHover(document, position, tokenSource.token) middleware.provideHover = undefined assert.strictEqual(middlewareCalled, true) }) test('Completion', async () => { const provider = client.getFeature(CompletionRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideCompletionItems(document, position, tokenSource.token, { triggerKind: CompletionTriggerKind.Invoked, triggerCharacter: ':' })) as CompletionItem[] isArray(result, CompletionItem) const item = result[0] assert.strictEqual(item.label, 'item') assert.strictEqual(item.insertText, 'text') assert.strictEqual(item.detail, undefined) isDefined(provider.resolveCompletionItem) const resolved = await provider.resolveCompletionItem(item, tokenSource.token) isDefined(resolved) assert.strictEqual(resolved.detail, 'detail') let middlewareCalled = 0 middleware.provideCompletionItem = (document, position, context, token, next) => { middlewareCalled++ return next(document, position, context, token) } middleware.resolveCompletionItem = (item, token, next) => { middlewareCalled++ return next(item, token) } await provider.provideCompletionItems(document, position, tokenSource.token, { triggerKind: CompletionTriggerKind.Invoked, triggerCharacter: ':' }) await provider.resolveCompletionItem(item, tokenSource.token) middleware.provideCompletionItem = undefined middleware.resolveCompletionItem = undefined assert.strictEqual(middlewareCalled, 2) }) test('SignatureHelpRequest', async () => { await helper.wait(50) let provider = client.getFeature(SignatureHelpRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideSignatureHelp(document, position, tokenSource.token, { isRetrigger: false, triggerKind: SignatureHelpTriggerKind.Invoked, triggerCharacter: ':' } ) assert.strictEqual(result.activeSignature, 1) assert.strictEqual(result.activeParameter, 1) isArray(result.signatures, SignatureInformation) const signature = result.signatures[0] assert.strictEqual(signature.label, 'label') assert.strictEqual(signature.documentation, 'doc') isArray(signature.parameters, ParameterInformation) const parameter = signature.parameters[0] assert.strictEqual(parameter.label, 'label') assert.strictEqual(parameter.documentation, 'doc') let middlewareCalled = false middleware.provideSignatureHelp = (d, p, c, t, n) => { middlewareCalled = true return n(d, p, c, t) } await provider.provideSignatureHelp(document, position, tokenSource.token, { isRetrigger: false, triggerKind: SignatureHelpTriggerKind.Invoked, triggerCharacter: ':' } ) middleware.provideSignatureHelp = undefined assert.ok(middlewareCalled) }) test('References', async () => { const provider = client.getFeature(ReferencesRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideReferences(document, position, { includeDeclaration: true }, tokenSource.token) isArray(result, Location, 2) for (let i = 0; i < result.length; i++) { const location = result[i] rangeEqual(location.range, i, i, i, i) assert.strictEqual(location.uri.toString(), document.uri.toString()) } let middlewareCalled = false middleware.provideReferences = (d, p, c, t, n) => { middlewareCalled = true return n(d, p, c, t) } await provider.provideReferences(document, position, { includeDeclaration: true }, tokenSource.token) middleware.provideReferences = undefined assert.ok(middlewareCalled) }) test('Document Highlight', async () => { const provider = client.getFeature(DocumentHighlightRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideDocumentHighlights(document, position, tokenSource.token) isArray(result, DocumentHighlight, 1) const highlight = result[0] assert.strictEqual(highlight.kind, DocumentHighlightKind.Read) rangeEqual(highlight.range, 2, 2, 2, 2) let middlewareCalled = false middleware.provideDocumentHighlights = (d, p, t, n) => { middlewareCalled = true return n(d, p, t) } await provider.provideDocumentHighlights(document, position, tokenSource.token) middleware.provideDocumentHighlights = undefined assert.ok(middlewareCalled) }) test('Code Actions', async () => { const provider = client.getFeature(CodeActionRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideCodeActions(document, range, { diagnostics: [] }, tokenSource.token)) as CodeAction[] assert.strictEqual(result.length, 3) const action = result[0] assert.strictEqual(action.title, 'title') assert.strictEqual(action.command?.title, 'title') assert.strictEqual(action.command?.command, 'test_command') let response = await commands.execute(action.command) expect(response).toEqual({ success: true }) const resolved = (await provider.resolveCodeAction(result[0], tokenSource.token)) assert.strictEqual(resolved?.title, 'resolved') let middlewareCalled = false middleware.provideCodeActions = (d, r, c, t, n) => { middlewareCalled = true return n(d, r, c, t) } await provider.provideCodeActions(document, range, { diagnostics: [] }, tokenSource.token) middleware.provideCodeActions = undefined assert.ok(middlewareCalled) middlewareCalled = false middleware.resolveCodeAction = (c, t, n) => { middlewareCalled = true return n(c, t) } await provider.resolveCodeAction!(result[0], tokenSource.token) middleware.resolveCodeAction = undefined assert.ok(middlewareCalled) let uri = URI.parse('lsptests://localhost/empty.bat').toString() let textDocument = TextDocument.create(uri, 'bat', 1, '\n') let res = (await provider.provideCodeActions(textDocument, range, { diagnostics: [] }, tokenSource.token)) as CodeAction[] expect(res).toBeUndefined() }) test('CodeLens', async () => { let feature = client.getFeature(CodeLensRequest.method) let state = feature.getState() expect((state as any).registrations).toBe(true) expect((state as any).matches).toBe(true) let tokenSource = new CancellationTokenSource() let codeLens = await languages.getCodeLens(document, tokenSource.token) expect(codeLens.length).toBe(2) let resolved = await languages.resolveCodeLens(codeLens[0], tokenSource.token) expect(resolved.command).toBeDefined() let fireRefresh = false let provider = feature.getProvider(document) provider.onDidChangeCodeLensEmitter.event(() => { fireRefresh = true }) await client.sendNotification('fireCodeLensRefresh') await helper.waitValue(() => fireRefresh, true) }) test('Progress', async () => { const progressToken = 'TEST-PROGRESS-TOKEN' const middlewareEvents: Array = [] let currentProgressResolver: (value: unknown) => void | undefined // Set up middleware that calls the current resolve function when it gets its 'end' progress event. middleware.handleWorkDoneProgress = (token: ProgressToken, params, next) => { if (token === progressToken) { middlewareEvents.push(params) if (params.kind === 'end') { setImmediate(currentProgressResolver) } } return next(token, params) } // Trigger multiple sample progress events. for (let i = 0; i < 2; i++) { await new Promise((resolve, reject) => { currentProgressResolver = resolve void client.sendRequest( new ProtocolRequestType('testing/sendSampleProgress'), {}, tokenSource.token, ).catch(reject) }) } middleware.handleWorkDoneProgress = undefined // Ensure all events were handled. assert.deepStrictEqual( middlewareEvents.map(p => p.kind), ['begin', 'report', 'end', 'begin', 'report', 'end'], ) await client.sendRequest( new ProtocolRequestType('testing/beginOnlyProgress'), {}, tokenSource.token, ) }) test('Progress percentage is an integer', async () => { const progressToken = 'TEST-PROGRESS-PERCENTAGE' const percentages: Array = [] let currentProgressResolver: (value: unknown) => void | undefined // Set up middleware that calls the current resolve function when it gets its 'end' progress event. middleware.handleWorkDoneProgress = (token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd, next) => { if (token === progressToken) { const percentage = params.kind === 'report' || params.kind === 'begin' ? params.percentage : undefined percentages.push(percentage) if (params.kind === 'end') { setImmediate(currentProgressResolver) } } return next(token, params) } // Trigger a progress event. await new Promise(resolve => { currentProgressResolver = resolve void client.sendRequest( new ProtocolRequestType('testing/sendPercentageProgress'), {}, tokenSource.token, ) }) middleware.handleWorkDoneProgress = undefined // Ensure percentages are rounded according to the spec assert.deepStrictEqual(percentages, [0, 50, undefined]) }) test('Document Formatting', async () => { const provider = client.getFeature(DocumentFormattingRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideDocumentFormattingEdits(document, { tabSize: 4, insertSpaces: false }, tokenSource.token) isArray(result, TextEdit) const edit = result[0] assert.strictEqual(edit.newText, 'insert') rangeEqual(edit.range, 0, 0, 0, 0) let middlewareCalled = true middleware.provideDocumentFormattingEdits = (d, c, t, n) => { middlewareCalled = true return n(d, c, t) } await provider.provideDocumentFormattingEdits(document, { tabSize: 4, insertSpaces: false }, tokenSource.token) middleware.provideDocumentFormattingEdits = undefined assert.ok(middlewareCalled) }) test('Document Range Formatting', async () => { const provider = client.getFeature(DocumentRangeFormattingRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideDocumentRangeFormattingEdits(document, range, { tabSize: 4, insertSpaces: false }, tokenSource.token) isArray(result, TextEdit) const edit = result[0] assert.strictEqual(edit.newText, '') rangeEqual(edit.range, 1, 1, 1, 2) let middlewareCalled = true middleware.provideDocumentRangeFormattingEdits = (d, r, c, t, n) => { middlewareCalled = true return n(d, r, c, t) } await provider.provideDocumentRangeFormattingEdits(document, range, { tabSize: 4, insertSpaces: false }, tokenSource.token) middleware.provideDocumentFormattingEdits = undefined assert.ok(middlewareCalled) }) test('Document on Type Formatting', async () => { const provider = client.getFeature(DocumentOnTypeFormattingRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideOnTypeFormattingEdits(document, position, 'a', { tabSize: 4, insertSpaces: false }, tokenSource.token) isArray(result, TextEdit) const edit = result[0] assert.strictEqual(edit.newText, 'replace') rangeEqual(edit.range, 2, 2, 2, 3) let middlewareCalled = true middleware.provideOnTypeFormattingEdits = (d, p, s, c, t, n) => { middlewareCalled = true return n(d, p, s, c, t) } await provider.provideOnTypeFormattingEdits(document, position, 'a', { tabSize: 4, insertSpaces: false }, tokenSource.token) middleware.provideDocumentFormattingEdits = undefined assert.ok(middlewareCalled) }) test('Rename', async () => { const provider = client.getFeature(RenameRequest.method).getProvider(document) isDefined(provider) isDefined(provider.prepareRename) const prepareResult = await provider.prepareRename(document, position, tokenSource.token) as Range rangeEqual(prepareResult, 1, 1, 1, 2) const renameResult = await provider.provideRenameEdits(document, position, 'newName', tokenSource.token) assert.ok(WorkspaceEdit.is(renameResult)) let middlewareCalled = 0 middleware.prepareRename = (d, p, t, n) => { middlewareCalled++ return n(d, p, t) } await provider.prepareRename(document, position, tokenSource.token) middleware.prepareRename = undefined middleware.provideRenameEdits = (d, p, w, t, n) => { middlewareCalled++ return n(d, p, w, t) } await provider.provideRenameEdits(document, position, 'newName', tokenSource.token) middleware.provideRenameEdits = undefined assert.strictEqual(middlewareCalled, 2) }) test('Document Link', async () => { const provider = client.getFeature(DocumentLinkRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideDocumentLinks(document, tokenSource.token) isArray(result, DocumentLink) const documentLink = result[0] rangeEqual(documentLink.range, 1, 1, 1, 2) let middlewareCalled = 0 middleware.provideDocumentLinks = (d, t, n) => { middlewareCalled++ return n(d, t) } await provider.provideDocumentLinks(document, tokenSource.token) middleware.provideDocumentLinks = undefined isDefined(provider.resolveDocumentLink) const resolved = await provider.resolveDocumentLink(documentLink, tokenSource.token) isDefined(resolved.target) assert.strictEqual(resolved.target.toString(), URI.file('/target.txt').toString()) middleware.resolveDocumentLink = (i, t, n) => { middlewareCalled++ return n(i, t) } await provider.resolveDocumentLink(documentLink, tokenSource.token) middleware.resolveDocumentLink = undefined assert.strictEqual(middlewareCalled, 2) }) test('Document Color', async () => { const provider = client.getFeature(DocumentColorRequest.method).getProvider(document) isDefined(provider) const colors = await provider.provideDocumentColors(document, tokenSource.token) isArray(colors, ColorInformation) const color = colors[0] rangeEqual(color.range, 1, 1, 1, 2) colorEqual(color.color, 1, 1, 1, 1) let middlewareCalled = 0 middleware.provideDocumentColors = (d, t, n) => { middlewareCalled++ return n(d, t) } await provider.provideDocumentColors(document, tokenSource.token) middleware.provideDocumentColors = undefined const presentations = await provider.provideColorPresentations(color.color, { document, range }, tokenSource.token) isArray(presentations, ColorPresentation) const presentation = presentations[0] assert.strictEqual(presentation.label, 'label') middleware.provideColorPresentations = (c, x, t, n) => { middlewareCalled++ return n(c, x, t) } await provider.provideColorPresentations(color.color, { document, range }, tokenSource.token) middleware.provideColorPresentations = undefined assert.strictEqual(middlewareCalled, 2) }) test('Goto Declaration', async () => { const provider = client.getFeature(DeclarationRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideDeclaration(document, position, tokenSource.token)) as Location uriEqual(result.uri, uri) rangeEqual(result.range, 1, 1, 1, 2) let middlewareCalled = false middleware.provideDeclaration = (document, position, token, next) => { middlewareCalled = true return next(document, position, token) } await provider.provideDeclaration(document, position, tokenSource.token) middleware.provideDeclaration = undefined assert.strictEqual(middlewareCalled, true) }) test('Folding Ranges', async () => { const feature = client.getFeature(FoldingRangeRequest.method) as FoldingRangeFeature const providerData = feature.getProvider(document) isDefined(providerData) const provider = providerData.provider isDefined(provider) const result = (await provider.provideFoldingRanges(document, {}, tokenSource.token)) isArray(result, FoldingRange, 1) const range = result[0] assert.strictEqual(range.startLine, 1) assert.strictEqual(range.endLine, 2) let middlewareCalled = true middleware.provideFoldingRanges = (d, c, t, n) => { middlewareCalled = true return n(d, c, t) } await provider.provideFoldingRanges(document, {}, tokenSource.token) middleware.provideFoldingRanges = undefined assert.ok(middlewareCalled) }) test('Goto Implementation', async () => { const provider = client.getFeature(ImplementationRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideImplementation(document, position, tokenSource.token)) as Location uriEqual(result.uri, uri) rangeEqual(result.range, 2, 2, 3, 3) let middlewareCalled = false middleware.provideImplementation = (document, position, token, next) => { middlewareCalled = true return next(document, position, token) } await provider.provideImplementation(document, position, tokenSource.token) middleware.provideImplementation = undefined assert.strictEqual(middlewareCalled, true) }) test('Selection Range', async () => { const provider = client.getFeature(SelectionRangeRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideSelectionRanges(document, [position], tokenSource.token)) isArray(result, SelectionRange, 1) const range = result[0] rangeEqual(range.range, 1, 2, 3, 4) let middlewareCalled = false middleware.provideSelectionRanges = (d, p, t, n) => { middlewareCalled = true return n(d, p, t) } await provider.provideSelectionRanges(document, [position], tokenSource.token) middleware.provideSelectionRanges = undefined assert.strictEqual(middlewareCalled, true) }) test('Type Definition', async () => { const provider = client.getFeature(TypeDefinitionRequest.method).getProvider(document) isDefined(provider) const result = (await provider.provideTypeDefinition(document, position, tokenSource.token)) as Location uriEqual(result.uri, uri) rangeEqual(result.range, 2, 2, 3, 3) let middlewareCalled = false middleware.provideTypeDefinition = (document, position, token, next) => { middlewareCalled = true return next(document, position, token) } await provider.provideTypeDefinition(document, position, tokenSource.token) middleware.provideTypeDefinition = undefined assert.strictEqual(middlewareCalled, true) }) test('Call Hierarchy', async () => { const provider = client.getFeature(CallHierarchyPrepareRequest.method).getProvider(document) isDefined(provider) const result = (await provider.prepareCallHierarchy(document, position, tokenSource.token)) as CallHierarchyItem[] expect(result.length).toBe(1) let middlewareCalled = false middleware.prepareCallHierarchy = (d, p, t, n) => { middlewareCalled = true return n(d, p, t) } await provider.prepareCallHierarchy(document, position, tokenSource.token) middleware.prepareCallHierarchy = undefined assert.strictEqual(middlewareCalled, true) const item = result[0] const incoming = (await provider.provideCallHierarchyIncomingCalls(item, tokenSource.token)) as CallHierarchyIncomingCall[] expect(incoming.length).toBe(1) assert.deepEqual(incoming[0].from, item) middlewareCalled = false middleware.provideCallHierarchyIncomingCalls = (i, t, n) => { middlewareCalled = true return n(i, t) } await provider.provideCallHierarchyIncomingCalls(item, tokenSource.token) middleware.provideCallHierarchyIncomingCalls = undefined assert.strictEqual(middlewareCalled, true) const outgoing = (await provider.provideCallHierarchyOutgoingCalls(item, tokenSource.token)) as CallHierarchyOutgoingCall[] expect(outgoing.length).toBe(1) assert.deepEqual(outgoing[0].to, item) middlewareCalled = false middleware.provideCallHierarchyOutgoingCalls = (i, t, n) => { middlewareCalled = true return n(i, t) } await provider.provideCallHierarchyOutgoingCalls(item, tokenSource.token) middleware.provideCallHierarchyOutgoingCalls = undefined assert.strictEqual(middlewareCalled, true) }) const referenceFileUri = URI.parse('/dummy-edit') function ensureReferenceEdit(edits: WorkspaceEdit, type: string, expectedLines: string[]) { // Ensure the edits are as expected. assert.strictEqual(edits.documentChanges?.length, 1) const edit = edits.documentChanges[0] as TextDocumentEdit assert.strictEqual(edit.edits.length, 1) assert.strictEqual(edit.textDocument.uri, referenceFileUri.path) const expectedTextEdit = edit.edits[0] as TextEdit assert.strictEqual(expectedTextEdit.newText.trim(), `${type}:\n${expectedLines.join('\n')}`.trim()) } async function ensureNotificationReceived(type: string, params: any) { const result = await client.sendRequest( new ProtocolRequestType('testing/lastFileOperationRequest'), {}, tokenSource.token, ) assert.strictEqual(result.type, type) assert.deepEqual(result.params, params) assert.deepEqual(result, { type, params }) } const createFiles = [ '/my/file.txt', '/my/file.js', '/my/folder/', // Static registration for tests is [operation]-static and *.txt '/my/created-static/file.txt', '/my/created-static/file.js', '/my/created-static/folder/', // Dynamic registration for tests is [operation]-dynamic and *.js '/my/created-dynamic/file.txt', '/my/created-dynamic/file.js', '/my/created-dynamic/folder/', ].map(p => URI.file(p)) const renameFiles = [ ['/my/file.txt', '/my-new/file.txt'], ['/my/file.js', '/my-new/file.js'], ['/my/folder/', '/my-new/folder/'], // Static registration for tests is [operation]-static and *.txt ['/my/renamed-static/file.txt', '/my-new/renamed-static/file.txt'], ['/my/renamed-static/file.js', '/my-new/renamed-static/file.js'], ['/my/renamed-static/folder/', '/my-new/renamed-static/folder/'], // Dynamic registration for tests is [operation]-dynamic and *.js ['/my/renamed-dynamic/file.txt', '/my-new/renamed-dynamic/file.txt'], ['/my/renamed-dynamic/file.js', '/my-new/renamed-dynamic/file.js'], ['/my/renamed-dynamic/folder/', '/my-new/renamed-dynamic/folder/'], ].map(([o, n]) => ({ oldUri: URI.file(o), newUri: URI.file(n) })) const deleteFiles = [ '/my/file.txt', '/my/file.js', '/my/folder/', // Static registration for tests is [operation]-static and *.txt '/my/deleted-static/file.txt', '/my/deleted-static/file.js', '/my/deleted-static/folder/', // Dynamic registration for tests is [operation]-dynamic and *.js '/my/deleted-dynamic/file.txt', '/my/deleted-dynamic/file.js', '/my/deleted-dynamic/folder/', ].map(p => URI.file(p)) test('File Operations - Will Create Files', async () => { const feature = client.getFeature(WillCreateFilesRequest.method) isDefined(feature) const sendCreateRequest = () => new Promise(async (resolve, reject) => { void feature.send({ token: CancellationToken.None, files: createFiles, waitUntil: resolve }) // If feature.send didn't call waitUntil synchronously then something went wrong. reject(new Error('Feature unexpectedly did not call waitUntil synchronously')) }) // Send the event and ensure the server responds with an edit referencing the // correct files. let edits = await sendCreateRequest() ensureReferenceEdit( edits, 'WILL CREATE', [ 'file:///my/created-static/file.txt', 'file:///my/created-static/folder/', 'file:///my/created-dynamic/file.js', 'file:///my/created-dynamic/folder/', ], ) // Add middleware that strips out any folders. middleware.workspace = middleware.workspace || {} middleware.workspace.willCreateFiles = (event, next) => next({ ...event, files: event.files.filter(f => !f.path.endsWith('/')), }) // Ensure we get the same results minus the folders that the middleware removed. edits = await sendCreateRequest() ensureReferenceEdit( edits, 'WILL CREATE', [ 'file:///my/created-static/file.txt', 'file:///my/created-dynamic/file.js', ], ) middleware.workspace.willCreateFiles = undefined }) test('File Operations - Did Create Files', async () => { const feature = client.getFeature(DidCreateFilesNotification.method) isDefined(feature) // Send the event and ensure the server reports the notification was sent. await feature.send({ files: createFiles }) await ensureNotificationReceived( 'create', { files: [ { uri: 'file:///my/created-static/file.txt' }, { uri: 'file:///my/created-static/folder/' }, { uri: 'file:///my/created-dynamic/file.js' }, { uri: 'file:///my/created-dynamic/folder/' }, ], }, ) // Add middleware that strips out any folders. middleware.workspace = middleware.workspace || {} middleware.workspace.didCreateFiles = (event, next) => next({ files: event.files.filter(f => !f.path.endsWith('/')), }) // Ensure we get the same results minus the folders that the middleware removed. await feature.send({ files: createFiles }) await ensureNotificationReceived( 'create', { files: [ { uri: 'file:///my/created-static/file.txt' }, { uri: 'file:///my/created-dynamic/file.js' }, ], }, ) middleware.workspace.didCreateFiles = undefined }) test('File Operations - Will Rename Files', async () => { const feature = client.getFeature(WillRenameFilesRequest.method) isDefined(feature) const sendRenameRequest = () => new Promise(async (resolve, reject) => { void feature.send({ files: renameFiles, waitUntil: resolve }) // If feature.send didn't call waitUntil synchronously then something went wrong. reject(new Error('Feature unexpectedly did not call waitUntil synchronously')) }) // Send the event and ensure the server responds with an edit referencing the // correct files. let edits = await sendRenameRequest() ensureReferenceEdit( edits, 'WILL RENAME', [ 'file:///my/renamed-static/file.txt -> file:///my-new/renamed-static/file.txt', 'file:///my/renamed-static/folder/ -> file:///my-new/renamed-static/folder/', 'file:///my/renamed-dynamic/file.js -> file:///my-new/renamed-dynamic/file.js', 'file:///my/renamed-dynamic/folder/ -> file:///my-new/renamed-dynamic/folder/', ], ) // Add middleware that strips out any folders. middleware.workspace = middleware.workspace || {} middleware.workspace.willRenameFiles = (event, next) => next({ ...event, files: event.files.filter(f => !f.oldUri.path.endsWith('/')), }) // Ensure we get the same results minus the folders that the middleware removed. edits = await sendRenameRequest() ensureReferenceEdit( edits, 'WILL RENAME', [ 'file:///my/renamed-static/file.txt -> file:///my-new/renamed-static/file.txt', 'file:///my/renamed-dynamic/file.js -> file:///my-new/renamed-dynamic/file.js', ], ) middleware.workspace.willRenameFiles = undefined }) test('File Operations - Did Rename Files', async () => { const feature = client.getFeature(DidRenameFilesNotification.method) isDefined(feature) // Send the event and ensure the server reports the notification was sent. await feature.send({ files: renameFiles }) await ensureNotificationReceived( 'rename', { files: [ { oldUri: 'file:///my/renamed-static/file.txt', newUri: 'file:///my-new/renamed-static/file.txt' }, { oldUri: 'file:///my/renamed-static/folder/', newUri: 'file:///my-new/renamed-static/folder/' }, { oldUri: 'file:///my/renamed-dynamic/file.js', newUri: 'file:///my-new/renamed-dynamic/file.js' }, { oldUri: 'file:///my/renamed-dynamic/folder/', newUri: 'file:///my-new/renamed-dynamic/folder/' }, ], }, ) // Add middleware that strips out any folders. middleware.workspace = middleware.workspace || {} middleware.workspace.didRenameFiles = (event, next) => next({ files: event.files.filter(f => !f.oldUri.path.endsWith('/')), }) // Ensure we get the same results minus the folders that the middleware removed. await feature.send({ files: renameFiles }) await ensureNotificationReceived( 'rename', { files: [ { oldUri: 'file:///my/renamed-static/file.txt', newUri: 'file:///my-new/renamed-static/file.txt' }, { oldUri: 'file:///my/renamed-dynamic/file.js', newUri: 'file:///my-new/renamed-dynamic/file.js' }, ], }, ) middleware.workspace.didRenameFiles = undefined }) test('File Operations - Will Delete Files', async () => { const feature = client.getFeature(WillDeleteFilesRequest.method) isDefined(feature) const sendDeleteRequest = () => new Promise(async (resolve, reject) => { void feature.send({ files: deleteFiles, waitUntil: resolve }) // If feature.send didn't call waitUntil synchronously then something went wrong. reject(new Error('Feature unexpectedly did not call waitUntil synchronously')) }) // Send the event and ensure the server responds with an edit referencing the // correct files. let edits = await sendDeleteRequest() ensureReferenceEdit( edits, 'WILL DELETE', [ 'file:///my/deleted-static/file.txt', 'file:///my/deleted-static/folder/', 'file:///my/deleted-dynamic/file.js', 'file:///my/deleted-dynamic/folder/', ], ) // Add middleware that strips out any folders. middleware.workspace = middleware.workspace || {} middleware.workspace.willDeleteFiles = (event, next) => next({ ...event, files: event.files.filter(f => !f.path.endsWith('/')), }) // Ensure we get the same results minus the folders that the middleware removed. edits = await sendDeleteRequest() ensureReferenceEdit( edits, 'WILL DELETE', [ 'file:///my/deleted-static/file.txt', 'file:///my/deleted-dynamic/file.js', ], ) middleware.workspace.willDeleteFiles = undefined }) test('File Operations - Did Delete Files', async () => { const feature = client.getFeature(DidDeleteFilesNotification.method) isDefined(feature) // Send the event and ensure the server reports the notification was sent. await feature.send({ files: deleteFiles }) await ensureNotificationReceived( 'delete', { files: [ { uri: 'file:///my/deleted-static/file.txt' }, { uri: 'file:///my/deleted-static/folder/' }, { uri: 'file:///my/deleted-dynamic/file.js' }, { uri: 'file:///my/deleted-dynamic/folder/' }, ], }, ) // Add middleware that strips out any folders. middleware.workspace = middleware.workspace || {} middleware.workspace.didDeleteFiles = (event, next) => next({ files: event.files.filter(f => !f.path.endsWith('/')), }) // Ensure we get the same results minus the folders that the middleware removed. await feature.send({ files: deleteFiles }) await ensureNotificationReceived( 'delete', { files: [ { uri: 'file:///my/deleted-static/file.txt' }, { uri: 'file:///my/deleted-dynamic/file.js' }, ], }, ) middleware.workspace.didDeleteFiles = undefined }) test('Semantic Tokens', async () => { const provider = client.getFeature(SemanticTokensRegistrationType.method).getProvider(document) const rangeProvider = provider?.range isDefined(rangeProvider) const rangeResult = await rangeProvider.provideDocumentRangeSemanticTokens(document, range, tokenSource.token) assert.ok(rangeResult !== undefined) let middlewareCalled = false middleware.provideDocumentRangeSemanticTokens = (d, r, t, n) => { middlewareCalled = true return n(d, r, t) } await rangeProvider.provideDocumentRangeSemanticTokens(document, range, tokenSource.token) middleware.provideDocumentRangeSemanticTokens = undefined assert.strictEqual(middlewareCalled, true) const fullProvider = provider?.full isDefined(fullProvider) const fullResult = await fullProvider.provideDocumentSemanticTokens(document, tokenSource.token) assert.ok(fullResult !== undefined) middlewareCalled = false middleware.provideDocumentSemanticTokens = (d, t, n) => { middlewareCalled = true return n(d, t) } await fullProvider.provideDocumentSemanticTokens(document, tokenSource.token) middleware.provideDocumentSemanticTokens = undefined assert.strictEqual(middlewareCalled, true) middlewareCalled = false middleware.provideDocumentSemanticTokensEdits = (d, i, t, n) => { middlewareCalled = true return n(d, i, t) } await fullProvider.provideDocumentSemanticTokensEdits!(document, '2', tokenSource.token) middleware.provideDocumentSemanticTokensEdits = undefined assert.strictEqual(middlewareCalled, true) let called = false provider.onDidChangeSemanticTokensEmitter.event(() => { called = true }) await client.sendNotification('fireSemanticTokensRefresh') await helper.waitValue(() => { return called }, true) }) test('Linked Editing Ranges', async () => { const provider = client.getFeature(LinkedEditingRangeRequest.method).getProvider(document) isDefined(provider) const result = await provider.provideLinkedEditingRanges(document, position, tokenSource.token) isArray(result.ranges, Range, 1) rangeEqual(result.ranges[0], 1, 1, 1, 1) let middlewareCalled = false middleware.provideLinkedEditingRange = (document, position, token, next) => { middlewareCalled = true return next(document, position, token) } await provider.provideLinkedEditingRanges(document, position, tokenSource.token) middleware.provideTypeDefinition = undefined assert.strictEqual(middlewareCalled, true) }) test('Document diagnostic pull', async () => { const provider = client.getFeature(DocumentDiagnosticRequest.method)?.getProvider(document) isDefined(provider) const result = await provider.diagnostics.provideDiagnostics(document, undefined, tokenSource.token) isDefined(result) isFullDocumentDiagnosticReport(result) const diag = result.items[0] rangeEqual(diag.range, 1, 1, 1, 1) assert.strictEqual(diag.message, 'diagnostic') let middlewareCalled = false middleware.provideDiagnostics = (document, previousResultId, token, next) => { middlewareCalled = true return next(document, previousResultId, token) } await provider.diagnostics.provideDiagnostics(document, undefined, tokenSource.token) middleware.provideDiagnostics = undefined assert.strictEqual(middlewareCalled, true) }) test('Workspace diagnostic pull', async () => { const provider = client.getFeature(DocumentDiagnosticRequest.method)?.getProvider(document) isDefined(provider) isDefined(provider.diagnostics.provideWorkspaceDiagnostics) await provider.diagnostics.provideWorkspaceDiagnostics([], tokenSource.token, result => { isDefined(result) isArray(result.items, undefined, 1) }) let middlewareCalled = false middleware.provideWorkspaceDiagnostics = (resultIds, token, reporter, next) => { middlewareCalled = true return next(resultIds, token, reporter) } await provider.diagnostics.provideWorkspaceDiagnostics([], tokenSource.token, () => {}) middleware.provideWorkspaceDiagnostics = undefined assert.strictEqual(middlewareCalled, true) }) test('Type Hierarchy', async () => { const provider = client.getFeature(TypeHierarchyPrepareRequest.method).getProvider(document) isDefined(provider) const result = await provider.prepareTypeHierarchy(document, position, tokenSource.token) isArray(result, undefined, 1) const item = result[0] let middlewareCalled = false middleware.prepareTypeHierarchy = (d, p, t, n) => { middlewareCalled = true return n(d, p, t) } await provider.prepareTypeHierarchy(document, position, tokenSource.token) middleware.prepareTypeHierarchy = undefined assert.strictEqual(middlewareCalled, true) const incoming = await provider.provideTypeHierarchySupertypes(item, tokenSource.token) isArray(incoming, undefined, 1) middlewareCalled = false middleware.provideTypeHierarchySupertypes = (i, t, n) => { middlewareCalled = true return n(i, t) } await provider.provideTypeHierarchySupertypes(item, tokenSource.token) middleware.provideTypeHierarchySupertypes = undefined assert.strictEqual(middlewareCalled, true) const outgoing = await provider.provideTypeHierarchySubtypes(item, tokenSource.token) isArray(outgoing, undefined, 1) middlewareCalled = false middleware.provideTypeHierarchySubtypes = (i, t, n) => { middlewareCalled = true return n(i, t) } await provider.provideTypeHierarchySubtypes(item, tokenSource.token) middleware.provideTypeHierarchySubtypes = undefined assert.strictEqual(middlewareCalled, true) }) test('Inline Values', async () => { const providerData = client.getFeature(InlineValueRequest.method).getProvider(document) isDefined(providerData) const provider = providerData.provider const results = (await provider.provideInlineValues(document, range, { frameId: 1, stoppedLocation: range }, tokenSource.token)) isArray(results, undefined, 3) for (const r of results) { rangeEqual(r.range, 1, 2, 3, 4) } // assert.ok(results[0] instanceof InlineValueText) assert.strictEqual((results[0] as InlineValueText).text, 'text') // assert.ok(results[1] instanceof InlineValueVariableLookup) assert.strictEqual((results[1] as InlineValueVariableLookup).variableName, 'variableName') // assert.ok(results[2] instanceof InlineValueEvaluatableExpression) assert.strictEqual((results[2] as InlineValueEvaluatableExpression).expression, 'expression') let middlewareCalled = false middleware.provideInlineValues = (d, r, c, t, n) => { middlewareCalled = true return n(d, r, c, t) } await provider.provideInlineValues(document, range, { frameId: 1, stoppedLocation: range }, tokenSource.token) middleware.provideInlineValues = undefined assert.strictEqual(middlewareCalled, true) }) test('Inlay Hints', async () => { let feature = client.getFeature(InlayHintRequest.method) as InlayHintsFeature const providerData = feature.getProvider(document) expect(feature.getProvider(TextDocument.create('term:///1', 'foo', 1, '\n'))).toBeUndefined() feature.register({ id: uuidv4(), registerOptions: { documentSelector: null } }) let res = feature.getRegistration([], { id: '1', workDoneProgress: '' } as any) expect(res).toEqual([undefined, undefined]) isDefined(providerData) const provider = providerData.provider const results = (await provider.provideInlayHints(document, range, tokenSource.token)) isArray(results, undefined, 2) const hint = results[0] positionEqual(hint.position, 1, 1) assert.strictEqual(hint.kind, InlayHintKind.Type) const label = hint.label isArray(label as [], InlayHintLabelPart, 1) assert.strictEqual((label as InlayHintLabelPart[])[0].value, 'type') let middlewareCalled = false middleware.provideInlayHints = (d, r, t, n) => { middlewareCalled = true return n(d, r, t) } await provider.provideInlayHints(document, range, tokenSource.token) middleware.provideInlayHints = undefined assert.strictEqual(middlewareCalled, true) assert.ok(typeof provider.resolveInlayHint === 'function') const resolvedHint = await provider.resolveInlayHint!(hint, tokenSource.token) assert.strictEqual((resolvedHint?.label as InlayHintLabelPart[])[0].tooltip, 'tooltip') let called = false await client.sendNotification('fireInlayHintsRefresh') provider.onDidChangeInlayHints(() => { called = true }) await helper.waitValue(() => { return called }, true) }) test('Inline Completions', async () => { const provider = client.getFeature(InlineCompletionRequest.method).getProvider(document) isDefined(provider) const results = (await provider.provideInlineCompletionItems(document, position, { triggerKind: 1, selectedCompletionInfo: { range, text: 'text' } }, tokenSource.token)) as InlineCompletionItem[] isArray(results, InlineCompletionItem, 1) rangeEqual(results[0].range!, 1, 2, 3, 4) assert.strictEqual(results[0].filterText!, 'te') assert.strictEqual(results[0].insertText, 'text inline') let middlewareCalled = false middleware.provideInlineCompletionItems = (d, r, c, t, n) => { middlewareCalled = true return n(d, r, c, t) } await provider.provideInlineCompletionItems(document, position, { triggerKind: 1, selectedCompletionInfo: undefined }, tokenSource.token) middleware.provideInlineCompletionItems = undefined assert.strictEqual(middlewareCalled, true) }) test('Workspace symbols', async () => { const providers = client.getFeature(WorkspaceSymbolRequest.method).getProviders() isDefined(providers) assert.strictEqual(providers.length, 2) const provider = providers[0] const results = await provider.provideWorkspaceSymbols('', tokenSource.token) isArray(results, undefined, 1) assert.strictEqual(results.length, 1) const symbol = await provider.resolveWorkspaceSymbol!(results[0], tokenSource.token) isDefined(symbol) rangeEqual(symbol.location['range'], 1, 2, 3, 4) }) test('Text Document Content', async () => { let feature = client.getFeature(TextDocumentContentRequest.method) const providers = feature?.getProviders() isDefined(providers) assert.strictEqual(providers.length, 1) const provider = providers[0].provider const result = await provider.provideTextDocumentContent(URI.parse('content-test:///test.txt'), tokenSource.token) assert.strictEqual(result, 'Some test content') feature.unregister('foo') expect(feature.getState()).toBeDefined() let middlewareCalled = false middleware.provideTextDocumentContent = (uri, token, next) => { middlewareCalled = true return next(uri, token) } await provider.provideTextDocumentContent(URI.parse('content-test:///test.txt'), tokenSource.token) middleware.provideTextDocumentContent = undefined assert.strictEqual(middlewareCalled, true) }) test('General middleware', async () => { let middlewareCallCount = 0 let throwError = false // Add a general middleware for both requests and notifications middleware.sendRequest = (type, param, token, next) => { middlewareCallCount++ return next(type, param, token) } middleware.sendNotification = (type, next, params) => { if (throwError) throw new Error('myerror') middlewareCallCount++ return next(type, params) } // Send a request const definitionProvider = client.getFeature(DefinitionRequest.method).getProvider(document) isDefined(definitionProvider) await definitionProvider.provideDefinition(document, position, tokenSource.token) // Send a notification const notificationProvider = client.getFeature(DidSaveTextDocumentNotification.method).getProvider(document) isDefined(notificationProvider) await notificationProvider.send(document) throwError = true await assert.rejects(async () => { await client.sendNotification('not_exists') }, /myerror/) // Verify that both the request and notification went through the middleware middleware.sendRequest = undefined middleware.sendNotification = undefined assert.strictEqual(middlewareCallCount, 2) }) test('applyEdit middleware', async () => { const middlewareEvents: Array = [] let currentProgressResolver: (value: unknown) => void | undefined let error = false middleware.workspace = middleware.workspace || {} middleware.workspace.handleApplyEdit = async (params, next) => { middlewareEvents.push(params) setImmediate(currentProgressResolver) if (error) return new ResponseError(ErrorCodes.InternalError, 'myerror') return next(params, tokenSource.token) } // Trigger sample applyEdit event. await new Promise(resolve => { currentProgressResolver = resolve void client.sendRequest( new ProtocolRequestType('testing/sendApplyEdit'), {}, tokenSource.token, ) }) // Ensure event was handled. assert.strictEqual(middlewareEvents.length, 1) assert.strictEqual(middlewareEvents[0].label, 'Apply Edit') error = true let called = false let spy = jest.spyOn(client, 'error').mockImplementation(() => { called = true }) await client.sendRequest( new ProtocolRequestType('testing/sendApplyEdit'), {}, tokenSource.token, ) await helper.waitValue(() => called, true) middleware.workspace.handleApplyEdit = undefined spy.mockRestore() }) }) namespace CrashNotification { export const type = new NotificationType0('test/crash') } class CrashClient extends LanguageClient { private resolve: (() => void) | undefined public onCrash: Promise constructor(id: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions) { super(id, name, serverOptions, clientOptions) this.onCrash = new Promise(resolve => { this.resolve = resolve }) } protected async handleConnectionClosed(): Promise { await super.handleConnectionClosed() this.resolve!() } } describe('sever tests', () => { test('Stop fails if server crashes after shutdown request', async () => { let file = path.join(__dirname, './server/crashOnShutdownServer.js') const serverOptions: ServerOptions = { module: file, transport: TransportKind.ipc, } const clientOptions: LanguageClientOptions = {} const client = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions) await client._start() await assert.rejects(async () => { await client.stop() }, /Pending response rejected since connection got disposed/) assert.strictEqual(client.needsStart(), true) assert.strictEqual(client.needsStop(), false) // Stopping again should be a no-op. await client.stop() assert.strictEqual(client.needsStart(), true) assert.strictEqual(client.needsStop(), false) await helper.wait(20) }) test('Stop not throw if server shutdown request times out', async () => { const serverOptions: ServerOptions = { module: path.join(__dirname, './server/timeoutOnShutdownServer.js'), transport: TransportKind.ipc, } const clientOptions: LanguageClientOptions = {} const client = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions) await client._start() await client.stop(10) }) test('Server can not be stopped when connection not exists', async () => { const serverOptions: ServerOptions = { module: path.join(__dirname, './server/testServer.js'), transport: TransportKind.ipc, } const clientOptions: LanguageClientOptions = {} const client = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions) let spy = jest.spyOn(client, 'createConnection' as any).mockReturnValue(Promise.reject(new Error('myerror'))) await assert.rejects(async () => { await client.start() }, Error) await assert.rejects(async () => { await client.stop() }, /Client is not running and can't be stopped/) spy.mockRestore() }) test('Test state change events', async () => { const serverOptions: ServerOptions = { module: path.join(__dirname, './server/nullServer.js'), transport: TransportKind.ipc, } const clientOptions: LanguageClientOptions = {} const client = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions) let state: State | undefined client.onDidChangeState(event => { state = event.newState }) await client._start() assert.strictEqual(state, State.Running, 'First start') await client.stop() assert.strictEqual(state, State.Stopped, 'First stop') await client._start() assert.strictEqual(state, State.Running, 'Second start') await client.stop() assert.strictEqual(state, State.Stopped, 'Second stop') }) test('Test state change events on crash', async () => { const serverOptions: ServerOptions = { module: path.join(__dirname, './server/crashServer.js'), transport: TransportKind.ipc, } const clientOptions: LanguageClientOptions = {} const client = new CrashClient('test svr', 'Test Language Server', serverOptions, clientOptions) let states: State[] = [] client.onDidChangeState(event => { states.push(event.newState) }) await client._start() assert.strictEqual(states.length, 2, 'First start') assert.strictEqual(states[0], State.Starting) assert.strictEqual(states[1], State.Running) states = [] await client.sendNotification(CrashNotification.type) await client.onCrash await client._start() assert.strictEqual(states.length, 3, 'Restart after crash') assert.strictEqual(states[0], State.Stopped) assert.strictEqual(states[1], State.Starting) assert.strictEqual(states[2], State.Running) states = [] await client.stop() assert.strictEqual(states.length, 1, 'After stop') assert.strictEqual(states[0], State.Stopped) }) }) describe('Server activation', () => { const uri: URI = URI.parse('lsptests://localhost/test.bat') const documentSelector: DocumentSelector = [{ scheme: 'lsptests', language: '*' }] const position: Position = Position.create(1, 1) let contentProviderDisposable!: Disposable beforeAll(async () => { contentProviderDisposable = workspace.registerTextDocumentContentProvider('lsptests', { provideTextDocumentContent: (_uri: URI) => { return [ 'REM @ECHO OFF' ].join('\n') } }) }) afterAll(async () => { contentProviderDisposable.dispose() }) function createClient(): LanguageClient { const serverModule = path.join(__dirname, './server/customServer.js') const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } } const clientOptions: LanguageClientOptions = { documentSelector, synchronize: {}, initializationOptions: {}, middleware: {}, }; (clientOptions as ({ $testMode?: boolean })).$testMode = true const result = new LanguageClient('test svr', 'Test Language Server', serverOptions, clientOptions) result.registerProposedFeatures() return result } test('Start server on request', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) const result: number = await client.sendRequest('request', { value: 10 }) assert.strictEqual(client.state, State.Running) assert.strictEqual(result, 11) await client.stop() }) test('Start server fails on request when stopped once', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) const result: number = await client.sendRequest('request', { value: 10 }) assert.strictEqual(client.state, State.Running) assert.strictEqual(result, 11) await client.stop() await assert.rejects(async () => { await client.sendRequest('request', { value: 10 }) }, /Client is not running/) }) test('Start server on notification', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) await client.sendNotification('notification') assert.strictEqual(client.state, State.Running) await client.stop() }) test('Not fails on notification when stopped once', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) await client.sendNotification('notification') assert.strictEqual(client.state, State.Running) await client.stop() await client.sendNotification('notification') }) test('Add pending request handler', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) let requestReceived = false client.onRequest('request', () => { requestReceived = true }) await client.sendRequest('triggerRequest') assert.strictEqual(requestReceived, true) await client.stop() }) test('Add pending notification handler', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) let notificationReceived = false client.onNotification('notification', () => { notificationReceived = true }) await client.sendRequest('triggerNotification') assert.strictEqual(notificationReceived, true) await client.stop() }) test('Starting disposed server fails', async () => { const client = createClient() await client._start() await client.dispose() await assert.rejects(async () => { await client._start() }, /Client got disposed and can't be restarted./) }) async function checkServerStart(client: LanguageClient, disposable: Disposable): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Server didn't start in 1000 ms.`)) }, 1000) client.onDidChangeState(event => { if (event.newState === State.Running) { clearTimeout(timeout) disposable.dispose() resolve() } }) }) } test('Start server on document open', async () => { await workspace.nvim.command('silent! %bwipeout!') const client = createClient() assert.strictEqual(client.state, State.Stopped) const started = checkServerStart(client, workspace.onDidOpenTextDocument(document => { if (workspace.match([{ scheme: 'lsptests', pattern: uri.fsPath }], document) > 0) { void client.start() } })) await workspace.openTextDocument(uri) await started await client.stop() }) test('Start server on language feature', async () => { const client = createClient() assert.strictEqual(client.state, State.Stopped) const started = checkServerStart(client, languages.registerDeclarationProvider(documentSelector, { provideDeclaration: async () => { await client._start() return undefined } })) await workspace.jumpTo(uri) await helper.doAction('declarations') await started await client.stop() }) }) ================================================ FILE: src/__tests__/client/fileSystemWatcher.test.ts ================================================ import path from 'path' import { DidChangeWatchedFilesNotification, DocumentSelector, Emitter, Event, FileChangeType } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { asRelativePattern } from '../../language-client/fileSystemWatcher' import { LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from '../../language-client/index' import { IFileSystemWatcher } from '../../types' import helper from '../helper' function createClient(fileEvents: IFileSystemWatcher | IFileSystemWatcher[] | undefined, middleware: Middleware = {}): LanguageClient { const serverModule = path.join(__dirname, './server/fileWatchServer.js') const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } } const documentSelector: DocumentSelector = [{ scheme: 'file' }] const clientOptions: LanguageClientOptions = { documentSelector, synchronize: { fileEvents }, initializationOptions: {}, middleware }; (clientOptions as ({ $testMode?: boolean })).$testMode = true const result = new LanguageClient('test', 'Test Language Server', serverOptions, clientOptions) return result } class CustomWatcher implements IFileSystemWatcher { public ignoreCreateEvents = false public ignoreChangeEvents = false public ignoreDeleteEvents = false private readonly _onDidCreate = new Emitter() public readonly onDidCreate: Event = this._onDidCreate.event private readonly _onDidChange = new Emitter() public readonly onDidChange: Event = this._onDidChange.event private readonly _onDidDelete = new Emitter() public readonly onDidDelete: Event = this._onDidDelete.event constructor() { } public fireCreate(uri: URI): void { this._onDidCreate.fire(uri) } public fireChange(uri: URI): void { this._onDidChange.fire(uri) } public fireDelete(uri: URI): void { this._onDidDelete.fire(uri) } public dispose() { } } beforeAll(async () => { await helper.setup() }) afterAll(async () => { await helper.shutdown() }) describe('FileSystemWatcherFeature', () => { it('should hook file events from client configuration', async () => { let res = asRelativePattern({ baseUri: { name: 'name', uri: '/tmp' }, pattern: '**' }) expect(res.baseUri.fsPath).toBe('/tmp') let client: LanguageClient let watcher = new CustomWatcher() let called = false let changes: FileChangeType[] = [] client = createClient([watcher], { workspace: { didChangeWatchedFile: async (event, next): Promise => { called = true if (event) { changes.push(event.type) } return next(event) } } }) let received: any[] client.onNotification('filesChange', params => { received = params.changes }) await client.start() expect(called).toBe(false) client.notifyFileEvent(undefined) await helper.wait(10) let uri = URI.file(__filename) watcher.fireCreate(uri) expect(called).toBe(true) watcher.fireChange(uri) watcher.fireDelete(uri) expect(changes).toEqual([1, 2, 3]) await helper.waitValue(() => { return received?.length }, 3) await client.stop() expect(received[2]).toEqual({ uri: uri.toString(), type: 3 }) }) it('should work with single watcher', async () => { let client: LanguageClient let watcher = new CustomWatcher() client = createClient(watcher, {}) let received: any[] client.onNotification('filesChange', params => { received = params.changes }) await client.start() let uri = URI.file(__filename) watcher.fireCreate(uri) await helper.waitValue(() => { return received?.length }, 1) let called = false let spy = jest.spyOn(client, 'sendNotification').mockImplementation(() => { called = true return Promise.reject(new Error('myerror')) }) watcher.fireChange(uri) await helper.waitValue(() => called, true) spy.mockRestore() await client.stop() }) it('should support dynamic registration', async () => { let client: LanguageClient client = createClient(undefined) await client.start() await helper.waitValue(async () => { let feature = client.getFeature(DidChangeWatchedFilesNotification.method) if (feature) await (feature as any)._notifyFileEvent() return feature != undefined }, true) await helper.waitValue(async () => { let feature = client.getFeature(DidChangeWatchedFilesNotification.method) let state = feature.getState() return (state as any).registrations }, true) await client.sendNotification('unwatch') await helper.waitValue(() => { let feature = client.getFeature(DidChangeWatchedFilesNotification.method) let state = feature.getState() return (state as any)?.registrations }, false) await client.stop() }) }) ================================================ FILE: src/__tests__/client/integration.test.ts ================================================ import * as assert from 'assert' import cp, { ChildProcess } from 'child_process' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { CancellationToken, DidCreateFilesNotification, Disposable, ErrorCodes, InlayHintRequest, LSPErrorCodes, MessageType, ResponseError, Trace, WorkDoneProgress } from 'vscode-languageserver-protocol' import { IPCMessageReader, IPCMessageWriter } from 'vscode-languageserver-protocol/node' import { MarkupKind, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import * as lsclient from '../../language-client' import { CloseAction, ErrorAction } from '../../language-client' import { FeatureState, LSPCancellationError, StaticFeature } from '../../language-client/features' import { DefaultErrorHandler, ErrorHandlerResult, InitializationFailedHandler } from '../../language-client/utils/errorHandler' import { disposeAll } from '../../util' import { CancellationError } from '../../util/errors' import * as extension from '../../util/extensionRegistry' import { Registry } from '../../util/registry' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() }) afterEach(() => { disposeAll(disposables) }) afterAll(async () => { await helper.shutdown() }) async function testLanguageServer(serverOptions: lsclient.ServerOptions, clientOpts?: lsclient.LanguageClientOptions): Promise { let clientOptions: lsclient.LanguageClientOptions = { documentSelector: ['css'], initializationOptions: {} } if (clientOpts) Object.assign(clientOptions, clientOpts) let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) await client.start() expect(client.initializeResult).toBeDefined() expect(client.started).toBe(true) return client } describe('SettingMonitor', () => { it('should setup SettingMonitor', async () => { let clientOptions: lsclient.LanguageClientOptions = { uriConverter: { code2Protocol: uri => uri.toString() }, initializationOptions: () => { return {} }, markdown: { supportHtml: true }, disableDynamicRegister: true } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) client.onNotification('customNotification', () => { }) client.onProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', () => { }) await client.start() await client.forceDocumentSync() await client.sendNotification('register') await helper.wait(30) expect(client.traceOutputChannel).toBeDefined() let monitor = new lsclient.SettingMonitor(client, 'html.enabled') helper.updateConfiguration('html.enabled', false) disposables.push(monitor.start()) await helper.waitValue(() => { return client.state }, lsclient.State.Stopped) helper.updateConfiguration('html.enabled', true, disposables) await helper.waitValue(() => { return client.state != lsclient.State.Stopped }, true) await client.onReady() await client.stop() }) it('should use SettingMonitor for primary setting', async () => { let clientOptions: lsclient.LanguageClientOptions = {} let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'node', args: [serverModule], transport: lsclient.TransportKind.stdio, options: { env: false } } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let monitor = new lsclient.SettingMonitor(client, 'TestServerEnabled') let spy = jest.spyOn(client, 'start').mockReturnValue(Promise.reject(new Error('myerror')) as any) disposables.push(monitor.start()) spy.mockRestore() await client.start() let called = false let s = jest.spyOn(client, 'stop').mockImplementation(() => { called = true return Promise.reject(new Error('myerror')) }) helper.updateConfiguration('TestServerEnabled', false) await helper.waitValue(() => called, true) s.mockRestore() await client.stop() }) }) describe('global functions', () => { it('should get working directory', async () => { let cwd = await lsclient.getServerWorkingDir() expect(cwd).toBeDefined() cwd = await lsclient.getServerWorkingDir({ cwd: 'not_exists' }) expect(cwd).toBeUndefined() }) it('should get main root', async () => { expect(lsclient.mainGetRootPath()).toBeUndefined() let uri = URI.file(__filename) await workspace.openResource(uri.toString()) expect(lsclient.mainGetRootPath()).toBeDefined() await workspace.nvim.command('bd!') }) it('should get runtime path', async () => { expect(lsclient.getRuntimePath('node', undefined)).toBe('node') expect(lsclient.getRuntimePath(__filename, undefined)).toBeDefined() let uri = URI.file(__filename) await workspace.openResource(uri.toString()) expect(lsclient.getRuntimePath('package.json', undefined)).toBeDefined() let name = path.basename(__filename) expect(lsclient.getRuntimePath(name, __dirname)).toBeDefined() }) it('should check debug mode', async () => { expect(lsclient.startedInDebugMode(['--debug'])).toBe(true) expect(lsclient.startedInDebugMode(undefined)).toBe(false) }) }) describe('Client events', () => { it('should start server', async () => { let clientOptions: lsclient.LanguageClientOptions = {} let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) disposables.push(client) await client.start() let called = false let spy = jest.spyOn(client, 'error').mockImplementation(() => { called = true }) await client.sendNotification('registerBad') await helper.waitValue(() => called, true) spy.mockRestore() { let spy = jest.spyOn(client['_connection'], 'trace').mockReturnValue(Promise.reject(new Error('myerror'))) client.trace = Trace.Compact spy.mockRestore() } }) it('should restart on error', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'node', args: [serverModule, '--stdio'] } let client = await testLanguageServer(serverOptions, { errorHandler: new DefaultErrorHandler('test', 2) }) let called = false let spy = jest.spyOn(client, 'start').mockImplementation((async () => { called = true throw new Error('myerror') }) as any) let sp: ChildProcess = client['_serverProcess'] sp.kill('SIGKILL') await helper.waitValue(() => called, true) spy.mockRestore() }) it('should not start on process exit', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'node', args: [serverModule, '--stdio'] } let clientOptions: lsclient.LanguageClientOptions = { documentSelector: ['css'], errorHandler: { error: () => ErrorAction.Shutdown, closed: () => { return { action: CloseAction.DoNotRestart, handled: true } } } } let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) await client.start() client['_state'] = lsclient.ClientState.Starting let sp: ChildProcess = client['_serverProcess'] sp.kill() await helper.waitValue(() => client['_state'], lsclient.ClientState.StartFailed) }) it('should register events before server start', async () => { let clientOptions: lsclient.LanguageClientOptions = {} let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let name = client.getExtensionName() expect(name).toBe('html') let n = 0 let disposable = client.onRequest('customRequest', () => { n++ disposable.dispose() return {} }) let dispose = client.onNotification('customNotification', () => { n++ dispose.dispose() }) let dis = client.onProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', p => { expect(p).toEqual({ kind: 'end', message: 'end message' }) n++ dis.dispose() }) disposables.push(client) await client.start() await client.sendNotification('send') await helper.waitValue(() => { return n }, 3) // let client = await testEventServer({ initEvent: true }) }) it('should register events after server start', async () => { let clientOptions: lsclient.LanguageClientOptions = { synchronize: {}, initializationOptions: { initEvent: true } } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) disposables.push(client) await client.start() let n = 0 let disposable = client.onRequest('customRequest', () => { n++ disposable.dispose() return {} }) let dispose = client.onNotification('customNotification', () => { n++ dispose.dispose() }) let dis = client.onProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', p => { expect(p).toEqual({ kind: 'end', message: 'end message' }) n++ dis.dispose() }) await client.sendNotification('send') await helper.waitValue(() => { return n }, 3) }) it('should send progress', async () => { let clientOptions: lsclient.LanguageClientOptions = { synchronize: {}, initializationOptions: { initEvent: true } } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let called = false client.onNotification('progressResult', res => { called = true expect(res).toEqual({ kind: 'begin', title: 'begin progress' }) }) await client.sendProgress(WorkDoneProgress.type, '4b3a71d0-2b3f-46af-be2c-2827f548579f', { kind: 'begin', title: 'begin progress' }) await client.start() await helper.waitValue(() => called, true) let spy = jest.spyOn(client['_connection'] as any, 'sendProgress').mockImplementation(() => { throw new Error('error') }) await expect(async () => { await client.sendProgress(WorkDoneProgress.type, '', { kind: 'begin', title: '' }) }).rejects.toThrow(Error) spy.mockRestore() let p = client.stop() await expect(async () => { await client._start() }).rejects.toThrow(Error) await p await expect(async () => { await client.sendProgress(WorkDoneProgress.type, '', { kind: 'begin', title: '' }) }).rejects.toThrow(/not running/) }) it('should use custom errorHandler', async () => { let throwError = false let called = false let result: ErrorHandlerResult | ErrorAction = { action: ErrorAction.Shutdown, handled: true } let clientOptions: lsclient.LanguageClientOptions = { synchronize: {}, errorHandler: { error: () => { return result }, closed: () => { called = true if (throwError) throw new Error('myerror') return CloseAction.DoNotRestart } }, initializationOptions: { initEvent: true } } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) disposables.push(client) throwError = true await assert.rejects(async () => { await client.sendRequest('bad', CancellationToken.Cancelled) }, /cancelled/) await client.sendRequest('doExit') await client.start() await helper.waitValue(() => { return called }, true) await client.handleConnectionError(new Error('error'), { jsonrpc: '' }, 1) result = ErrorAction.Continue await client.handleConnectionError(new Error('error'), { jsonrpc: '' }, 1) }) it('should handle message events', async () => { let clientOptions: lsclient.LanguageClientOptions = { synchronize: {}, } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) expect(client.hasPendingResponse).toBeUndefined() disposables.push(client) await client.start() await client.sendNotification('logMessage') await client.sendNotification('showMessage') let types = [MessageType.Error, MessageType.Warning, MessageType.Info, MessageType.Log] let times = 0 let result = true const mockMessageFunctions = function(): Disposable { let names = ['showErrorMessage', 'showWarningMessage', 'showInformationMessage'] let fns: Function[] = [] for (let name of names) { let spy = jest.spyOn(window as any, name).mockImplementation(() => { times++ return Promise.resolve(result) }) fns.push(() => { spy.mockRestore() }) } return Disposable.create(() => { for (let fn of fns) { fn() } }) } disposables.push(mockMessageFunctions()) for (const t of types) { await client.sendNotification('requestMessage', { type: t }) } await helper.waitValue(() => { return times >= 3 }, true) let filename = path.join(os.tmpdir(), uuid()) let uri = URI.file(filename) fs.writeFileSync(filename, 'foo', 'utf8') let spy = jest.spyOn(workspace, 'openResource').mockImplementation(() => { return Promise.resolve() }) let called = false let s = jest.spyOn(window, 'selectRange').mockImplementation(() => { called = true return Promise.reject(new Error('failed')) }) await client.sendNotification('showDocument', { external: true, uri: 'lsptest:///1' }) await client.sendNotification('showDocument', { uri: 'lsptest:///1', takeFocus: false }) await client.sendNotification('showDocument', { uri: uri.toString() }) await client.sendNotification('showDocument', { uri: uri.toString(), selection: Range.create(0, 0, 1, 0) }) await helper.waitValue(() => called, true) spy.mockRestore() s.mockRestore() fs.unlinkSync(filename) await helper.waitValue(() => { return client.hasPendingResponse }, false) }) it('should invoke showDocument middleware', async () => { let called = false let clientOptions: lsclient.LanguageClientOptions = { synchronize: {}, middleware: { window: { showDocument: async (params, token, next) => { called = true let res = await next(params, token) return res as any } } } } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let uri = URI.file(__filename) await client.start() await client.sendNotification('showDocument', { uri: uri.toString() }) await helper.waitValue(() => called, true) await client.restart() await client.stop() }) }) describe('Client integration', () => { it('should initialize from function', async () => { async function testServer(serverOptions: lsclient.ServerOptions) { let clientOptions: lsclient.LanguageClientOptions = {} let client = new lsclient.LanguageClient('HTML', serverOptions, clientOptions) await client.start() await client.dispose() void client.dispose() } await testServer(() => { let module = path.join(__dirname, './server/eventServer.js') let sp = cp.fork(module, ['--node-ipc'], { cwd: process.cwd() }) return Promise.resolve({ reader: new IPCMessageReader(sp), writer: new IPCMessageWriter(sp) }) }) await testServer(() => { let module = path.join(__dirname, './server/eventServer.js') let sp = cp.fork(module, ['--stdio'], { cwd: process.cwd(), execArgv: [], silent: true, }) return Promise.resolve({ reader: sp.stdout, writer: sp.stdin }) }) await testServer(() => { let module = path.join(__dirname, './server/eventServer.js') let sp = cp.fork(module, ['--stdio'], { cwd: process.cwd(), execArgv: [], silent: true, }) return Promise.resolve({ process: sp, detached: false }) }) await testServer(() => { let module = path.join(__dirname, './server/eventServer.js') let sp = cp.fork(module, ['--stdio'], { cwd: process.cwd(), execArgv: [], silent: true, }) return Promise.resolve(sp) }) }) it('should initialize use IPC channel', async () => { helper.updateConfiguration('css.trace.server.verbosity', 'verbose', disposables) helper.updateConfiguration('css.trace.server.format', 'json', disposables) let uri = URI.file(__filename) await workspace.loadFile(uri.toString()) let serverModule = path.join(__dirname, './server/testInitializeResult.js') let serverOptions: lsclient.ServerOptions = { run: { module: serverModule, transport: lsclient.TransportKind.ipc }, debug: { module: serverModule, transport: lsclient.TransportKind.ipc, options: { execArgv: [] } } } let clientOptions: lsclient.LanguageClientOptions = { rootPatterns: ['.vim'], requireRootPattern: true, documentSelector: ['css'], synchronize: {}, initializationOptions: {}, middleware: { handleDiagnostics: (uri, diagnostics, next) => { assert.equal(uri, "uri:/test.ts") assert.ok(Array.isArray(diagnostics)) assert.equal(diagnostics.length, 0) next(uri, diagnostics) } } } let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions, true) assert.ok(client.isInDebugMode) await client.start() await helper.waitValue(() => client.diagnostics.has('uri:/test.ts'), true) await client.restart() assert.deepEqual(client.initializeResult.customResults, { hello: 'world' }) await client.stop() await assert.rejects(async () => { let options: any = {} let client = new lsclient.LanguageClient('css', 'Test Language Server', options, clientOptions) await client.start() }, /Unsupported/) await assert.rejects(async () => { let options: lsclient.ServerOptions = { command: 'node', transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('css', 'Test Language Server', options, clientOptions) await client.start() }, /not supported/) await assert.rejects(async () => { let opts: any = { stdio: 'ignore' } let options: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc, options: opts } let client = new lsclient.LanguageClient('css', 'Test Language Server', options, clientOptions) await client.start() }, /without stdio/) }) it('should initialize use stdio', async () => { helper.updateConfiguration('css.trace.server.verbosity', 'verbose', disposables) helper.updateConfiguration('css.trace.server.format', 'text', disposables) let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio } let client = await testLanguageServer(serverOptions, { workspaceFolder: { name: 'test', uri: URI.file(__dirname).toString() }, outputChannel: window.createOutputChannel('test'), traceOutputChannel: window.createOutputChannel('test-trace'), markdown: {}, disabledFeatures: ['pullDiagnostic'], revealOutputChannelOn: lsclient.RevealOutputChannelOn.Info, outputChannelName: 'custom', connectionOptions: { cancellationStrategy: { sender: {} } as any, maxRestartCount: 10, }, stdioEncoding: 'utf8', errorHandler: { error: () => { return lsclient.ErrorAction.Continue }, closed: () => { return { action: CloseAction.DoNotRestart, handled: true } } }, progressOnInitialization: true, disableMarkdown: true, disableDiagnostics: true }) assert.deepStrictEqual(client.supportedMarkupKind, [MarkupKind.PlainText]) assert.strictEqual(client.name, 'Test Language Server') assert.strictEqual(client.diagnostics, undefined) expect(client.traceOutputChannel).toBeDefined() client.traceMessage('message') client.traceMessage('message', {}) client.trace = Trace.Verbose let d = client.start() let token = CancellationToken.Cancelled let sp: ChildProcess = client['_serverProcess'] expect(sp instanceof ChildProcess).toBe(true) sp.stdout.emit('error', new Error('my error')) client.handleFailedRequest(DidCreateFilesNotification.type, token, undefined, '') await expect(async () => { let error = new ResponseError(LSPErrorCodes.RequestCancelled, 'request cancelled') client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') }).rejects.toThrow(CancellationError) await expect(async () => { let error = new ResponseError(LSPErrorCodes.RequestCancelled, 'request cancelled', 'cancelled') client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') }).rejects.toThrow(LSPCancellationError) await expect(async () => { let error = new Error('failed') client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') }).rejects.toThrow(Error) let error = new ResponseError(LSPErrorCodes.ContentModified, 'content changed') client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') error = new ResponseError(ErrorCodes.PendingResponseRejected, '') client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') await expect(async () => { let error = new ResponseError(LSPErrorCodes.ContentModified, 'content changed') client.handleFailedRequest(InlayHintRequest.type, undefined, error, '') }).rejects.toThrow(CancellationError) await client.stop() client.info('message', new Error('my error'), true) client.warn('message', 'error', true) client.warn('message', 0, true) client.logFailedRequest({ method: 'method' }, new Error('error')) let err = new ResponseError(LSPErrorCodes.RequestCancelled, 'response error') client.logFailedRequest('', err) assert.strictEqual(client.diagnostics, undefined) await client.handleConnectionError(new Error('test'), undefined, 0) d.dispose() }) it('should initialize use pipe', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.pipe } let client = await testLanguageServer(serverOptions, { ignoredRootPaths: [workspace.root] }) expect(client.serviceState).toBeDefined() await client.stop() await assert.rejects(async () => { let option: lsclient.ServerOptions = { command: 'foobar', transport: lsclient.TransportKind.pipe } await testLanguageServer(option, {}) }, /ENOENT/) }) it('should initialize use socket', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, options: { env: { NODE_SOCKET_TEST: 1 } }, transport: { kind: lsclient.TransportKind.socket, port: 8088 } } let client = await testLanguageServer(serverOptions) await client.stop() let option: lsclient.ServerOptions = { command: 'node', args: [serverModule], transport: { kind: lsclient.TransportKind.socket, port: 9088 } } client = await testLanguageServer(option, {}) await client.sendNotification('printMessage') await helper.waitValue(() => { return client.outputChannel.content.match('Stderr') != null }, true) // avoid pending response error await helper.wait(50) await client.stop() await assert.rejects(async () => { let option: lsclient.ServerOptions = { command: 'foobar', transport: { kind: lsclient.TransportKind.socket, port: 9998 } } await testLanguageServer(option, {}) }, /ENOENT/) }) it('should initialize as command', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'node', args: [serverModule, '--stdio'] } let client = await testLanguageServer(serverOptions) await client.stop() }) it('should register features', async () => { let features: StaticFeature[] = [] let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'node', args: [serverModule, '--stdio'] } let clientOptions: lsclient.LanguageClientOptions = { documentSelector: ['css'], initializationOptions: {} } let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) let called = false class SimpleStaticFeature implements StaticFeature { public method = 'method' public fillClientCapabilities(capabilities): void { // Optionally add capabilities your feature supports capabilities.experimental = capabilities.experimental || {} capabilities.experimental.simpleStaticFeature = true } public preInitialize(): void { called = true } public initialize(): void { } public getState(): FeatureState { return { kind: 'static' } } public dispose(): void { } } features.push(new SimpleStaticFeature()) client.registerFeatures(features) await client.start() expect(called).toBe(true) await client.stop() }) it('should not throw as command', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'not_exists', args: [serverModule, '--stdio'] } let clientOptions: lsclient.LanguageClientOptions = { documentSelector: ['css'], initializationOptions: {} } let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) await assert.rejects(async () => { await client.start() }, /failed/) await expect(async () => { await client['$start']() }).rejects.toThrow(/failed/) }) it('should logMessage', async () => { let called = false let outputChannel = { name: 'empty', content: '', append: () => { called = true }, appendLine: () => { called = true }, clear: () => {}, show: () => {}, hide: () => {}, dispose: () => {} } helper.updateConfiguration('css.trace.server.verbosity', 'verbose', disposables) let serverOptions: lsclient.ServerOptions = { command: 'node', args: [path.join(__dirname, './server/eventServer.js'), '--stdio'] } let client = await testLanguageServer(serverOptions, { outputChannel, initializationOptions: { trace: true } }) expect(called).toBe(true) await client.stop() }) it('should use console for messages', async () => { let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { command: 'node', args: [serverModule, '--stdio'] } let client = await testLanguageServer(serverOptions) let fn = jest.fn() let spy = jest.spyOn(console, 'log').mockImplementation(() => { fn() }) let s = jest.spyOn(console, 'error').mockImplementation(() => { fn() }) client.switchConsole() client.info('message', { info: 'info' }) client.warn('message', { info: 'info' }) client.error('message', { info: 'info' }) client.info('message', { info: 'info' }) client.switchConsole() s.mockRestore() spy.mockRestore() await client.stop() expect(fn).toHaveBeenCalled() }) it('should check version on apply workspaceEdit', async () => { let uri = URI.file(__filename) await workspace.loadFile(uri.toString()) let clientOptions: lsclient.LanguageClientOptions = { documentSelector: [{ scheme: 'file' }], initializationOptions: {}, } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio, } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let res client.onNotification('result', p => { res = p }) let disposable = client.start() await disposable await client.sendNotification('edits') await helper.waitValue(() => { return res }, { applied: false }) disposable.dispose() await client.stop() }) it('should apply simple workspaceEdit', async () => { let clientOptions: lsclient.LanguageClientOptions = { initializationOptions: {}, } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.stdio, } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let res client.onNotification('result', p => { res = p }) await client.start() await client.sendNotification('simpleEdit') await helper.waitValue(() => { return res != null }, true) expect(res).toEqual({ applied: true }) await client.stop() }) it('should handle error on initialize', async () => { let client: lsclient.LanguageClient let progressOnInitialization = false async function startServer(handler: InitializationFailedHandler | undefined, key = 'throwError'): Promise { let clientOptions: lsclient.LanguageClientOptions = { initializationFailedHandler: handler, progressOnInitialization, initializationOptions: { [key]: true }, connectionOptions: { maxRestartCount: 1 } } let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc, } client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) await client.start() return client } let messageReturn = {} let spy = jest.spyOn(window, 'showErrorMessage').mockImplementation(() => { return Promise.resolve(messageReturn as any) }) let n = 0 await expect(async () => { await startServer(() => { n++ return n == 1 }) }).rejects.toThrow(Error) await helper.waitValue(() => { return n }, 2) await expect(async () => { await startServer(undefined) }).rejects.toThrow(Error) await expect(async () => { await startServer(undefined, 'normalThrow') }).rejects.toThrow(Error) progressOnInitialization = true await expect(async () => { client = await startServer(undefined, 'utf8') }).rejects.toThrow(/Unsupported position encoding/) await helper.waitValue(() => client.state, lsclient.State.Stopped) await client.stop() spy.mockRestore() }) it('should attach extension name', async () => { let clientOptions: lsclient.LanguageClientOptions = {} let serverModule = path.join(__dirname, './server/eventServer.js') let serverOptions: lsclient.ServerOptions = { module: serverModule, transport: lsclient.TransportKind.ipc } let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) let registry = Registry.as(extension.Extensions.ExtensionContribution) let filepath = path.join(os.tmpdir(), 'single') registry.registerExtension('single', { name: 'single', directory: os.tmpdir(), filepath }) client['stack'] = `\n\n${filepath}:1:1` let obj = {} client.attachExtensionName(obj) expect(typeof client.getExtensionName()).toBe('string') expect(obj['__extensionName']).toBe('single') registry.unregistExtension('single') await client.dispose() }) }) ================================================ FILE: src/__tests__/client/progressPart.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Emitter, Event, NotificationHandler, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport } from 'vscode-languageserver-protocol' import { ProgressContext, ProgressPart } from '../../language-client/progressPart' import helper from '../helper' type ProgressType = WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterEach(async () => { await helper.reset() }) afterAll(async () => { await helper.shutdown() }) describe('ProgressPart', () => { function createClient(): ProgressContext & { fire: (ev: ProgressType) => void, token: string | undefined } { let _onDidProgress = new Emitter() let onDidProgress: Event = _onDidProgress.event let notificationToken: string | undefined return { id: 'test', get token() { return notificationToken }, fire(ev) { _onDidProgress.fire(ev) }, onProgress(_, __, handler: NotificationHandler) { return onDidProgress(ev => { void handler(ev as any) }) }, sendNotification(_, params) { notificationToken = (params as any).token } } } it('should not start if cancelled', async () => { let client = createClient() let p = new ProgressPart(client, '0c7faec8-e36c-4cde-9815-95635c37d696') p.report({ kind: 'report', message: 'msg' }) p.cancel() expect(p.begin({ kind: 'begin', title: 'canceleld' })).toBe(false) }) it('should report progress', async () => { let client = createClient() let p = new ProgressPart(client, '0c7faec8-e36c-4cde-9815-95635c37d696') p.begin({ kind: 'begin', title: 'p', percentage: 1, cancellable: true }) await helper.wait(30) p.report({ kind: 'report', message: 'msg', percentage: 10 }) await helper.wait(10) p.report({ kind: 'report', message: 'msg', percentage: 50 }) await helper.wait(10) p.done('finished') }) it('should close notification on cancel', async () => { helper.updateConfiguration('notification.statusLineProgress', false) let client = createClient() let p = new ProgressPart(client, '0c7faec8-e36c-4cde-9815-95635c37d696') let started = p.begin({ kind: 'begin', title: 'canceleld' }) expect(started).toBe(true) p.cancel() p.cancel() let winids = await nvim.call('coc#notify#win_list') as number[] await helper.wait(30) expect(winids.length).toBe(1) let win = nvim.createWindow(winids[0]) let closing = await win.getVar('closing') expect(closing).toBe(1) }) it('should send notification on cancel', async () => { helper.updateConfiguration('notification.statusLineProgress', false) let client = createClient() let token = '0c7faec8-e36c-4cde-9815-95635c37d696' let p = new ProgressPart(client, token) let started = p.begin({ kind: 'begin', title: 'canceleld', cancellable: true }) expect(started).toBe(true) for (let i = 0; i < 10; i++) { await helper.wait(30) let winids = await nvim.call('coc#notify#win_list') as number[] if (winids.length == 1) break } await helper.wait(30) nvim.call('coc#float#close_all', [], true) await helper.waitValue(() => { return client.token }, token) }) }) ================================================ FILE: src/__tests__/client/server/configServer.js ================================================ 'use strict' const {createConnection, ConfigurationRequest, DidChangeConfigurationNotification} = require('vscode-languageserver/node') const {URI} = require('vscode-uri') const connection = createConnection() console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) connection.onInitialize((_params) => { return {capabilities: {}} }) connection.onNotification('pull0', () => { void connection.sendRequest(ConfigurationRequest.type, { items: [{ scopeUri: URI.file(__filename).toString() }] }) }) connection.onNotification('pull1', () => { void connection.sendRequest(ConfigurationRequest.type, { items: [{ section: 'http' }, { section: 'editor.cpp.format' }, { section: 'unknown' }] }) }) connection.onNotification(DidChangeConfigurationNotification.type, params => { void connection.sendNotification('configurationChange', params) }) connection.listen() ================================================ FILE: src/__tests__/client/server/crashOnShutdownServer.js ================================================ 'use strict' Object.defineProperty(exports, "__esModule", {value: true}) const node_1 = require("vscode-languageserver/node") const connection = (0, node_1.createConnection)() connection.onInitialize((_params) => { return {capabilities: {}} }) connection.onShutdown(() => { process.exit(100) }) connection.listen() ================================================ FILE: src/__tests__/client/server/crashServer.js ================================================ "use strict" Object.defineProperty(exports, "__esModule", {value: true}) const node_1 = require('vscode-languageserver/node') let CrashNotification; (function (CrashNotification) { CrashNotification.type = new node_1.NotificationType0('test/crash') })(CrashNotification || (CrashNotification = {})) const connection = (0, node_1.createConnection)() connection.onInitialize((_params) => { return { capabilities: {} } }) connection.onNotification(CrashNotification.type, () => { process.exit(100) }) connection.listen() ================================================ FILE: src/__tests__/client/server/customServer.js ================================================ "use strict" Object.defineProperty(exports, "__esModule", {value: true}) const node_1 = require("vscode-languageserver/node") const connection = (0, node_1.createConnection)() connection.onInitialize((_params) => { return { capabilities: {} } }) connection.onRequest('request', (param) => { return param.value + 1 }) connection.onNotification('notification', () => { }) connection.onRequest('triggerRequest', async () => { await connection.sendRequest('request') }) connection.onRequest('triggerNotification', async () => { await connection.sendNotification('notification') }) connection.listen() ================================================ FILE: src/__tests__/client/server/diagnosticServer.js ================================================ 'use strict' const {createConnection, ResponseError, LSPErrorCodes, DiagnosticRefreshRequest, DocumentDiagnosticReportKind, Diagnostic, Range, DiagnosticSeverity, TextDocuments, TextDocumentSyncKind} = require('vscode-languageserver/node') const {TextDocument} = require('vscode-languageserver-textdocument') let documents = new TextDocuments(TextDocument) const connection = createConnection() console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) let options documents.listen(connection) connection.onInitialize((params) => { options = params.initializationOptions || {} const interFileDependencies = options.interFileDependencies !== false const workspaceDiagnostics = options.workspaceDiagnostics === true const identifier = options.identifier ?? '6d52eff6-96c7-4fd1-910f-f060bcffb23f' return { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, diagnosticProvider: { identifier, interFileDependencies, workspaceDiagnostics } } } }) let count = 0 let saveCount = 0 connection.languages.diagnostics.on((params) => { let uri = params.textDocument.uri if (uri.endsWith('error')) return Promise.reject(new Error('server error')) if (uri.endsWith('cancel')) return new ResponseError(LSPErrorCodes.ServerCancelled, 'cancel', {retriggerRequest: false}) if (uri.endsWith('retrigger')) return new ResponseError(LSPErrorCodes.ServerCancelled, 'retrigger', {retriggerRequest: true}) if (uri.endsWith('change')) count++ if (uri.endsWith('save')) saveCount++ if (uri.endsWith('empty')) return null if (uri.endsWith('unchanged')) return { kind: DocumentDiagnosticReportKind.Unchanged, resultId: '1' } return { kind: DocumentDiagnosticReportKind.Full, items: [ Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) ] } }) let workspaceCount = 0 connection.languages.diagnostics.onWorkspace((params, _, __, reporter) => { if (params.previousResultIds.length > 0) { return new Promise((resolve, reject) => { setTimeout(() => { reporter.report({ items: [{ kind: DocumentDiagnosticReportKind.Full, uri: 'uri1', version: 1, items: [ Diagnostic.create(Range.create(1, 0, 1, 1), 'diagnostic', DiagnosticSeverity.Error) ] }] }) }, 10) setTimeout(() => { reporter.report(null) }, 15) setTimeout(() => { reporter.report({ items: [{ kind: DocumentDiagnosticReportKind.Full, uri: 'uri2', version: 1, items: [ Diagnostic.create(Range.create(2, 0, 2, 1), 'diagnostic', DiagnosticSeverity.Error) ] }] }) }, 20) setTimeout(() => { resolve({items: []}) }, 50) }) } workspaceCount++ if (workspaceCount == 2) { return new ResponseError(LSPErrorCodes.ServerCancelled, 'changed') } return { items: [{ kind: DocumentDiagnosticReportKind.Full, uri: 'uri', version: 1, items: [ Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) ] }] } }) connection.onNotification('fireRefresh', () => { void connection.sendRequest(DiagnosticRefreshRequest.type) }) connection.onRequest('getChangeCount', () => { return count }) connection.onRequest('getSaveCount', () => { return saveCount }) connection.onRequest('getWorkspaceCount', () => { return workspaceCount }) connection.onRequest('sendDiagnostics', async (_, __) => { const uri = 'file:///abc.txt' const diagnostics = [{ severity: DiagnosticSeverity.Warning, range: { start: {line: 0, character: 0}, end: {line: 0, character: 5} }, message: "Example warning: Check your code!", source: "ex" }] connection.sendDiagnostics({uri, diagnostics}) }) connection.listen() ================================================ FILE: src/__tests__/client/server/dynamicServer.js ================================================ 'use strict' const {createConnection, TextDocumentContentRefreshRequest, ProtocolRequestType, Range, TextDocumentSyncKind, Command, RenameRequest, WorkspaceSymbolRequest, SemanticTokensRegistrationType, CodeActionRequest, ConfigurationRequest, DidChangeConfigurationNotification, InlineValueRefreshRequest, ExecuteCommandRequest, CompletionRequest, WorkspaceFoldersRequest, ResponseError, ErrorCodes} = require('vscode-languageserver/node') const connection = createConnection() console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) let options let disposables = [] let prepareResponse let configuration let folders let foldersEvent const id = 'b346648e-88e0-44e3-91e3-52fd6addb8c7' connection.onInitialize((params) => { options = params.initializationOptions || {} let changeNotifications = options.changeNotifications ?? id return { capabilities: { inlineValueProvider: {}, executeCommandProvider: { }, documentSymbolProvider: options.label ? {label: 'test'} : true, textDocumentSync: TextDocumentSyncKind.Full, renameProvider: options.prepareRename ? {prepareProvider: true} : true, workspaceSymbolProvider: true, codeLensProvider: { resolveProvider: options.noResolve !== true }, documentLinkProvider: { resolveProvider: options.noResolve !== true }, inlayHintProvider: { resolveProvider: options.noResolve !== true }, workspace: { workspaceFolders: { changeNotifications }, fileOperations: { // Static reg is folders + .txt files with operation kind in the path didCreate: { filters: [ {scheme: 'lsptest', pattern: {glob: '**/*', matches: 'file', options: {}}}, {scheme: 'file', pattern: {glob: '**/*', matches: 'file', options: {ignoreCase: false}}} ] }, didRename: { filters: [ {scheme: 'file', pattern: {glob: '**/*', matches: 'folder'}}, {scheme: 'file', pattern: {glob: '**/*', matches: 'file'}} ] }, didDelete: { filters: [{scheme: 'file', pattern: {glob: '**/*'}}] }, willCreate: { filters: [{scheme: 'file', pattern: {glob: '**/*'}}] }, willRename: { filters: [ {scheme: 'file', pattern: {glob: '**/*', matches: 'folder'}}, {scheme: 'file', pattern: {glob: '**/*', matches: 'file'}} ] }, willDelete: { filters: [{scheme: 'file', pattern: {glob: '**/*'}}] }, }, textDocumentContent: options.textDocumentContent ? {id, schemes: ['lsptest']} : undefined }, } } }) connection.onInitialized(() => { void connection.client.register(RenameRequest.type, { prepareProvider: options.prepareRename }).then(d => { d.dispose() }) void connection.client.register(WorkspaceSymbolRequest.type, { resolveProvider: true }).then(d => { disposables.push(d) }) let full = false if (options.delta) { full = {delta: true} } else if (options.noResolve) { full = {delta: false} } void connection.client.register(SemanticTokensRegistrationType.method, { full, range: options.rangeTokens, legend: { tokenTypes: [], tokenModifiers: [] }, }) void connection.client.register(CodeActionRequest.method, { resolveProvider: false }) void connection.client.register(DidChangeConfigurationNotification.type, {section: undefined}) void connection.client.register(ExecuteCommandRequest.type, { commands: ['test_command', 'other_command'] }).then(d => { disposables.push(d) }) void connection.client.register(CompletionRequest.type, { documentSelector: [{language: 'vim'}] }).then(d => { disposables.push(d) }) void connection.client.register(CompletionRequest.type, { triggerCharacters: ['/'], }).then(d => { disposables.push(d) }) }) let lastFileOperationRequest connection.workspace.onDidCreateFiles(params => {lastFileOperationRequest = {type: 'create', params}}) connection.workspace.onDidRenameFiles(params => {lastFileOperationRequest = {type: 'rename', params}}) connection.workspace.onDidDeleteFiles(params => {lastFileOperationRequest = {type: 'delete', params}}) connection.workspace.onWillRenameFiles(params => {lastFileOperationRequest = {type: 'willRename', params}}) connection.workspace.onWillDeleteFiles(params => {lastFileOperationRequest = {type: 'willDelete', params}}) // connection.onDidChangeWorkspaceFolders(e => { // foldersEvent = params // }) connection.onCompletion(_params => { return [ {label: 'item', insertText: 'text'} ] }) connection.onCompletionResolve(item => { item.detail = 'detail' return item }) connection.onRequest( new ProtocolRequestType('testing/lastFileOperationRequest'), () => { return lastFileOperationRequest }, ) connection.onNotification('unregister', () => { for (let d of disposables) { d.dispose() disposables = [] } }) connection.onDocumentSymbol(() => { return [] }) connection.onExecuteCommand(param => { if (param.command === 'test_command') { return {success: true} } throw new ResponseError(ErrorCodes.InvalidRequest, `${param?.command} not exists.`) }) connection.languages.semanticTokens.onDelta(() => { return { resultId: '3', data: [] } }) connection.onRequest('setPrepareResponse', param => { prepareResponse = param }) connection.onNotification('pullConfiguration', () => { configuration = connection.sendRequest(ConfigurationRequest.type, { items: [{section: 'foo'}, {}] }) }) connection.onRequest('getConfiguration', () => { return configuration }) connection.onRequest('getFolders', () => { return folders }) connection.onRequest('getFoldersEvent', () => { return foldersEvent }) connection.onNotification('fireInlineValueRefresh', () => { void connection.sendRequest(InlineValueRefreshRequest.type) }) connection.onNotification('fireDocumentContentRefresh', () => { void connection.sendRequest(TextDocumentContentRefreshRequest.type, {uri: 'lsptest:///2'}) void connection.sendRequest(TextDocumentContentRefreshRequest.type, {uri: 'untitled:///1'}) }) connection.onNotification('requestFolders', async () => { folders = await connection.sendRequest(WorkspaceFoldersRequest.type) }) connection.onPrepareRename(() => { return prepareResponse }) connection.onCodeAction(() => { return [ Command.create('title', 'editor.action.triggerSuggest') ] }) connection.onWorkspaceSymbol(() => { return [] }) connection.onWorkspaceSymbolResolve(item => { return item }) connection.onCodeLens(params => { return [{range: Range.create(0, 0, 0, 3)}, {range: Range.create(1, 0, 1, 3)}] }) connection.onCodeLensResolve(codelens => { return {range: codelens.range, command: {title: 'format', command: 'format'}} }) connection.listen() ================================================ FILE: src/__tests__/client/server/errorServer.js ================================================ "use strict" const {createConnection, ResponseError} = require("vscode-languageserver/node") const connection = createConnection() connection.onInitialize((_params) => { return { capabilities: {} } }) connection.onSignatureHelp(_params => { return new ResponseError(-32803, 'failed') }) connection.listen() ================================================ FILE: src/__tests__/client/server/eventServer.js ================================================ 'use strict' const {createConnection, TextEdit, RenameRequest, ProtocolRequestType, TextDocuments, Range, DiagnosticSeverity, Location, Diagnostic, DiagnosticRelatedInformation, PositionEncodingKind, WorkDoneProgress, ResponseError, LogMessageNotification, MessageType, ShowMessageNotification, ShowMessageRequest, ShowDocumentRequest, ApplyWorkspaceEditRequest, TextDocumentSyncKind, Position, RegistrationType} = require('vscode-languageserver/node') const {TextDocument} = require('vscode-languageserver-textdocument') let documents = new TextDocuments(TextDocument) const connection = createConnection() console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) let options documents.listen(connection) connection.onInitialize((params) => { options = params.initializationOptions || {} if (options.throwError) { setTimeout(() => { process.exit() }, 10) return new ResponseError(1, 'message', {retry: true}) } if (options.normalThrow) { setTimeout(() => { process.exit() }, 10) throw new Error('normal throw error') } if (options.utf8) { return {capabilities: {positionEncoding: PositionEncodingKind.UTF8}} } if (options.trace) { setTimeout(() => { connection.tracer.log('This is a trace message') connection.tracer.log('This is a trace message', {'info': 'verbose info'}) }, 1) } return { capabilities: { textDocumentSync: TextDocumentSyncKind.Full } } }) connection.onNotification('diagnostics', () => { let diagnostics = [] let related = [] let uri = 'lsptest:///2' related.push(DiagnosticRelatedInformation.create(Location.create(uri, Range.create(0, 0, 0, 1)), 'dup')) related.push(DiagnosticRelatedInformation.create(Location.create(uri, Range.create(0, 0, 1, 0)), 'dup')) diagnostics.push(Diagnostic.create(Range.create(0, 0, 1, 0), 'msg', DiagnosticSeverity.Error, undefined, undefined, related)) void connection.sendDiagnostics({uri: 'lsptest:///1', diagnostics}) void connection.sendDiagnostics({uri: 'lsptest:///3', version: 1, diagnostics}) }) connection.onNotification('simpleEdit', async () => { let res = await connection.sendRequest(ApplyWorkspaceEditRequest.type, {edit: {documentChanges: []}}) void connection.sendNotification('result', res) }) connection.onNotification('register', async () => { void connection.client.register(RenameRequest.type, { prepareProvider: false }) }) connection.onNotification('registerBad', async () => { void connection.client.register(new ProtocolRequestType('not_exists'), {}) }) connection.onNotification('edits', async () => { let uris = documents.keys() let res = await connection.sendRequest(ApplyWorkspaceEditRequest.type, { edit: { documentChanges: uris.map(uri => { return { textDocument: {uri, version: documents.get(uri).version + 1}, edits: [TextEdit.insert(Position.create(0, 0), 'foo')] } }) } }) void connection.sendNotification('result', res) }) connection.onNotification('send', () => { void connection.sendRequest('customRequest') void connection.sendNotification('customNotification') void connection.sendProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', {kind: 'end', message: 'end message'}) }) connection.onNotification('logMessage', () => { void connection.sendNotification(LogMessageNotification.type, {type: MessageType.Debug, message: 'msg'}) void connection.sendNotification(LogMessageNotification.type, {type: MessageType.Error, message: 'msg'}) void connection.sendNotification(LogMessageNotification.type, {type: MessageType.Info, message: 'msg'}) void connection.sendNotification(LogMessageNotification.type, {type: MessageType.Log, message: 'msg'}) void connection.sendNotification(LogMessageNotification.type, {type: MessageType.Warning, message: 'msg'}) }) connection.onNotification('showMessage', () => { void connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Error, message: 'msg'}) void connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Info, message: 'msg'}) void connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Log, message: 'msg'}) void connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Warning, message: 'msg'}) }) connection.onNotification('requestMessage', async params => { await connection.sendRequest(ShowMessageRequest.type, {type: params.type, message: 'msg', actions: [{title: 'open'}]}) }) connection.onNotification('showDocument', async params => { await connection.sendRequest(ShowDocumentRequest.type, params) }) connection.onProgress(WorkDoneProgress.type, '4b3a71d0-2b3f-46af-be2c-2827f548579f', (params) => { void connection.sendNotification('progressResult', params) }) connection.onNotification('printMessage', () => { process.stdin.write('stdin\n') process.stdout.write('stdout\n') }) connection.onRequest('doExit', () => { setTimeout(() => { process.exit(1) }, 30) }) connection.listen() ================================================ FILE: src/__tests__/client/server/fileWatchServer.js ================================================ 'use strict' const {createConnection, DidChangeWatchedFilesNotification} = require('vscode-languageserver/node') const {URI} = require('vscode-uri') const connection = createConnection() console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) connection.onInitialize((_params) => { return {capabilities: {}} }) let disposables = [] connection.onInitialized(() => { void connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: [{ globPattern: '**/jsconfig.json', }, { globPattern: '**/*.ts', kind: 1 }, { globPattern: { baseUri: URI.file(process.cwd()).toString(), pattern: '**/*.vim' }, kind: 1 }, { globPattern: '**/*.js', kind: 2 }, { globPattern: -1 }] }).then(d => { disposables.push(d) }) void connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: null }).then(d => { disposables.push(d) }) }) connection.onNotification(DidChangeWatchedFilesNotification.type, params => { void connection.sendNotification('filesChange', params) }) connection.onNotification('unwatch', () => { for (let d of disposables) { d.dispose() } }) connection.listen() ================================================ FILE: src/__tests__/client/server/nullServer.js ================================================ "use strict" Object.defineProperty(exports, "__esModule", {value: true}) const node_1 = require("vscode-languageserver/node") const connection = (0, node_1.createConnection)() connection.onInitialize((_params) => { return { capabilities: {} } }) connection.onShutdown(() => { }) connection.listen() ================================================ FILE: src/__tests__/client/server/testDocuments.js ================================================ const {ResponseError, LSPErrorCodes} = require('vscode-languageserver/node') const ls = require('vscode-languageserver/node') const {TextDocument} = require('vscode-languageserver-textdocument') let connection = ls.createConnection() let documents = new ls.TextDocuments(TextDocument) let lastOpenEvent let lastCloseEvent let lastChangeEvent let lastWillSave let lastDidSave documents.onDidOpen(e => { lastOpenEvent = {uri: e.document.uri, version: e.document.version} }) documents.onDidClose(e => { lastCloseEvent = {uri: e.document.uri} }) documents.onDidChangeContent(e => { lastChangeEvent = {uri: e.document.uri, text: e.document.getText()} }) documents.onWillSave(e => { lastWillSave = {uri: e.document.uri} }) documents.onWillSaveWaitUntil(e => { let uri = e.document.uri if (uri.endsWith('error.vim')) throw new ResponseError(LSPErrorCodes.ContentModified, 'content changed') if (!uri.endsWith('foo.vim')) return [] return [ls.TextEdit.insert(ls.Position.create(0, 0), 'abc')] }) documents.onDidSave(e => { lastDidSave = {uri: e.document.uri} }) documents.listen(connection) console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) let opts connection.onInitialize(params => { opts = params.initializationOptions let capabilities = { textDocumentSync: { openClose: true, change: ls.TextDocumentSyncKind.Full, willSave: true, willSaveWaitUntil: true, save: true } } return {capabilities} }) connection.onRequest('getLastOpen', () => { return lastOpenEvent }) connection.onRequest('getLastClose', () => { return lastCloseEvent }) connection.onRequest('getLastChange', () => { return lastChangeEvent }) connection.onRequest('getLastWillSave', () => { return lastWillSave }) connection.onRequest('getLastDidSave', () => { return lastDidSave }) let disposables = [] connection.onNotification('registerDocumentSync', () => { let opt = {documentSelector: [{language: 'vim'}]} void connection.client.register(ls.DidOpenTextDocumentNotification.type, opt).then(d => { disposables.push(d) }) void connection.client.register(ls.DidCloseTextDocumentNotification.type, opt).then(d => { disposables.push(d) }) void connection.client.register(ls.DidChangeTextDocumentNotification.type, Object.assign({ syncKind: opts.none === true ? ls.TextDocumentSyncKind.None : ls.TextDocumentSyncKind.Incremental }, opt)).then(d => { disposables.push(d) }) void connection.client.register(ls.WillSaveTextDocumentNotification.type, opt).then(d => { disposables.push(d) }) void connection.client.register(ls.WillSaveTextDocumentWaitUntilRequest.type, opt).then(d => { disposables.push(d) }) }) connection.onNotification('unregisterDocumentSync', () => { for (let dispose of disposables) { dispose.dispose() } disposables = [] }) connection.listen() ================================================ FILE: src/__tests__/client/server/testFileWatcher.js ================================================ const languageserver = require('vscode-languageserver') let connection = languageserver.createConnection() let documents = new languageserver.TextDocuments() documents.listen(connection) connection.onInitialize(() => { let capabilities = { textDocumentSync: documents.syncKind } return { capabilities } }) connection.onInitialized(() => { connection.sendRequest('client/registerCapability', { registrations: [{ id: 'didChangeWatchedFiles', method: 'workspace/didChangeWatchedFiles', registerOptions: { watchers: [{ globPattern: "**" }] } }] }) }) let received connection.onNotification('workspace/didChangeWatchedFiles', params => { received = params }) connection.onRequest('custom/received', async () => { return received }) connection.listen() ================================================ FILE: src/__tests__/client/server/testInitializeResult.js ================================================ 'use strict' Object.defineProperty(exports, "__esModule", {value: true}) const tslib_1 = require("tslib") const assert = tslib_1.__importStar(require("assert")) const vscode_languageserver_1 = require("vscode-languageserver/node") let connection = vscode_languageserver_1.createConnection() connection.onInitialize((params) => { assert.equal(params.capabilities.workspace.applyEdit, true) assert.equal(params.capabilities.workspace.workspaceEdit.documentChanges, true) assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, [vscode_languageserver_1.ResourceOperationKind.Create, vscode_languageserver_1.ResourceOperationKind.Rename, vscode_languageserver_1.ResourceOperationKind.Delete]) assert.equal(params.capabilities.workspace.workspaceEdit.failureHandling, vscode_languageserver_1.FailureHandlingKind.Undo) assert.equal(params.capabilities.textDocument.completion.completionItem.deprecatedSupport, true) assert.equal(params.capabilities.textDocument.completion.completionItem.preselectSupport, true) assert.equal(params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport, true) assert.equal(params.capabilities.textDocument.rename.prepareSupport, true) let valueSet = params.capabilities.textDocument.completion.completionItemKind.valueSet assert.equal(valueSet[0], 1) assert.equal(valueSet[valueSet.length - 1], vscode_languageserver_1.CompletionItemKind.TypeParameter) let capabilities = { textDocumentSync: 1, completionProvider: {resolveProvider: true, triggerCharacters: ['"', ':']}, hoverProvider: true, renameProvider: { prepareProvider: true } } return {capabilities, customResults: {"hello": "world"}} }) connection.onInitialized(() => { void connection.sendDiagnostics({uri: "uri:/test.ts", diagnostics: []}) void connection.sendDiagnostics({uri: "uri:/not_exists.ts", diagnostics: [], version: 1}) }) // Listen on the connection connection.listen() ================================================ FILE: src/__tests__/client/server/testServer.js ================================================ const assert = require('assert') const {URI} = require('vscode-uri') const { createConnection, CompletionItemKind, ResourceOperationKind, FailureHandlingKind, DiagnosticTag, CompletionItemTag, TextDocumentSyncKind, MarkupKind, SignatureInformation, ParameterInformation, Location, Range, DocumentHighlight, DocumentHighlightKind, CodeAction, Command, TextEdit, Position, DocumentLink, ColorInformation, Color, ColorPresentation, FoldingRange, ProposedFeatures, SelectionRange, SymbolKind, ProtocolRequestType, WorkDoneProgress, SignatureHelpRequest, SemanticTokensRefreshRequest, WorkDoneProgressCreateRequest, CodeLensRefreshRequest, InlayHintRefreshRequest, WorkspaceSymbolRequest, DidChangeConfigurationNotification} = require('vscode-languageserver/node') const { DidCreateFilesNotification, DidRenameFilesNotification, DidDeleteFilesNotification, InlineCompletionItem, WillCreateFilesRequest, WillRenameFilesRequest, WillDeleteFilesRequest, InlayHint, InlayHintLabelPart, InlayHintKind, DocumentDiagnosticReportKind, Diagnostic, DiagnosticSeverity, InlineValueText, InlineValueVariableLookup, InlineValueEvaluatableExpression, ApplyWorkspaceEditRequest, DocumentSymbol } = require('vscode-languageserver-protocol') let connection = createConnection(ProposedFeatures.all) console.log = connection.console.log.bind(connection.console) console.error = connection.console.error.bind(connection.console) let disposables = [] connection.onInitialize(params => { assert.equal((params.capabilities.workspace).applyEdit, true) assert.equal(params.capabilities.workspace.workspaceEdit.documentChanges, true) assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, ['create', 'rename', 'delete']) assert.equal(params.capabilities.workspace.workspaceEdit.failureHandling, FailureHandlingKind.Undo) assert.equal(params.capabilities.workspace.workspaceEdit.normalizesLineEndings, true) assert.equal(params.capabilities.workspace.workspaceEdit.changeAnnotationSupport.groupsOnLabel, false) assert.equal(params.capabilities.workspace.symbol.resolveSupport.properties[0], 'location.range') assert.equal(params.capabilities.textDocument.completion.completionItem.deprecatedSupport, true) assert.equal(params.capabilities.textDocument.completion.completionItem.preselectSupport, true) assert.equal(params.capabilities.textDocument.completion.completionItem.tagSupport.valueSet.length, 1) assert.equal(params.capabilities.textDocument.completion.completionItem.tagSupport.valueSet[0], CompletionItemTag.Deprecated) assert.equal(params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport, true) assert.equal(params.capabilities.textDocument.definition.linkSupport, true) assert.equal(params.capabilities.textDocument.declaration.linkSupport, true) assert.equal(params.capabilities.textDocument.implementation.linkSupport, true) assert.equal(params.capabilities.textDocument.typeDefinition.linkSupport, true) assert.equal(params.capabilities.textDocument.rename.prepareSupport, true) assert.equal(params.capabilities.textDocument.publishDiagnostics.relatedInformation, true) assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet.length, 2) assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet[0], DiagnosticTag.Unnecessary) assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet[1], DiagnosticTag.Deprecated) assert.equal(params.capabilities.textDocument.documentLink.tooltipSupport, true) assert.equal(params.capabilities.textDocument.inlineValue.dynamicRegistration, true) assert.equal(params.capabilities.textDocument.inlayHint.dynamicRegistration, true) assert.equal(params.capabilities.textDocument.inlayHint.resolveSupport.properties[0], 'tooltip') let valueSet = params.capabilities.textDocument.completion.completionItemKind.valueSet assert.equal(valueSet[0], 1) assert.equal(valueSet[valueSet.length - 1], CompletionItemKind.TypeParameter) assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, [ResourceOperationKind.Create, ResourceOperationKind.Rename, ResourceOperationKind.Delete]) assert.equal(params.capabilities.workspace.fileOperations.willCreate, true) let diagnosticClientCapabilities = params.capabilities.textDocument.diagnostic assert.equal(diagnosticClientCapabilities.dynamicRegistration, true) assert.equal(diagnosticClientCapabilities.relatedDocumentSupport, true) const capabilities = { textDocumentSync: TextDocumentSyncKind.Full, definitionProvider: true, hoverProvider: true, signatureHelpProvider: { triggerCharacters: [','], retriggerCharacters: [';'] }, completionProvider: {resolveProvider: true, triggerCharacters: ['"', ':']}, referencesProvider: true, documentHighlightProvider: true, codeActionProvider: { resolveProvider: true }, codeLensProvider: { resolveProvider: true }, documentFormattingProvider: true, documentRangeFormattingProvider: { rangesSupport: true }, documentOnTypeFormattingProvider: { firstTriggerCharacter: ':' }, renameProvider: { prepareProvider: true }, documentLinkProvider: { resolveProvider: true }, documentSymbolProvider: true, colorProvider: true, declarationProvider: true, foldingRangeProvider: true, implementationProvider: { documentSelector: [{language: '*'}] }, selectionRangeProvider: true, inlineValueProvider: {}, inlineCompletionProvider: {}, inlayHintProvider: { resolveProvider: true }, typeDefinitionProvider: { id: '82671a9a-2a69-4e9f-a8d7-e1034eaa0d2e', documentSelector: [{language: '*'}] }, callHierarchyProvider: true, semanticTokensProvider: { legend: { tokenTypes: [], tokenModifiers: [] }, range: true, full: { delta: true } }, workspace: { fileOperations: { // Static reg is folders + .txt files with operation kind in the path didCreate: { filters: [{scheme: 'file', pattern: {glob: '**/created-static/**{/,/*.txt}'}}] }, didRename: { filters: [ {scheme: 'file', pattern: {glob: '**/renamed-static/**/', matches: 'folder'}}, {scheme: 'file', pattern: {glob: '**/renamed-static/**/*.txt', matches: 'file'}} ] }, didDelete: { filters: [{scheme: 'file', pattern: {glob: '**/deleted-static/**{/,/*.txt}'}}] }, willCreate: { filters: [{scheme: 'file', pattern: {glob: '**/created-static/**{/,/*.txt}'}}] }, willRename: { filters: [ {scheme: 'file', pattern: {glob: '**/renamed-static/**/', matches: 'folder'}}, {scheme: 'file', pattern: {glob: '**/renamed-static/**/*.txt', matches: 'file'}} ] }, willDelete: { filters: [{scheme: 'file', pattern: {glob: '**/deleted-static/**{/,/*.txt}'}}] }, }, textDocumentContent: { schemes: ['content-test'] } }, linkedEditingRangeProvider: true, diagnosticProvider: { identifier: 'da348dc5-c30a-4515-9d98-31ff3be38d14', interFileDependencies: true, workspaceDiagnostics: true }, typeHierarchyProvider: true, workspaceSymbolProvider: { resolveProvider: true }, notebookDocumentSync: { notebookSelector: [{ notebook: {notebookType: 'jupyter-notebook'}, cells: [{language: 'python'}] }] } } return {capabilities, customResults: {hello: 'world'}} }) connection.onInitialized(() => { // Dynamic reg is folders + .js files with operation kind in the path void connection.client.register(DidCreateFilesNotification.type, { filters: [{scheme: 'file', pattern: {glob: '**/created-dynamic/**{/,/*.js}'}}] }) void connection.client.register(DidRenameFilesNotification.type, { filters: [ {scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/', matches: 'folder'}}, {scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/*.js', matches: 'file'}} ] }) void connection.client.register(DidDeleteFilesNotification.type, { filters: [{scheme: 'file', pattern: {glob: '**/deleted-dynamic/**{/,/*.js}'}}] }) void connection.client.register(WillCreateFilesRequest.type, { filters: [{scheme: 'file', pattern: {glob: '**/created-dynamic/**{/,/*.js}'}}] }) void connection.client.register(WillRenameFilesRequest.type, { filters: [ {scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/', matches: 'folder'}}, {scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/*.js', matches: 'file'}} ] }) void connection.client.register(WillDeleteFilesRequest.type, { filters: [{scheme: 'file', pattern: {glob: '**/deleted-dynamic/**{/,/*.js}'}}] }) void connection.client.register(SignatureHelpRequest.type, { triggerCharacters: [':'], retriggerCharacters: [':'] }).then(d => { disposables.push(d) }) void connection.client.register(WorkspaceSymbolRequest.type, { workDoneProgress: false, resolveProvider: true }).then(d => { disposables.push(d) }) void connection.client.register(DidChangeConfigurationNotification.type, { section: 'http' }).then(d => { disposables.push(d) }) void connection.client.register(DidCreateFilesNotification.type, { filters: [{ pattern: { glob: '**/renamed-dynamic/**/', matches: 'folder', options: { ignoreCase: true } } }] }).then(d => { disposables.push(d) }) }) connection.onNotification('unregister', () => { for (let d of disposables) { d.dispose() disposables = [] } }) connection.onCodeLens(params => { return [{range: Range.create(0, 0, 0, 3)}, {range: Range.create(1, 0, 1, 3)}] }) connection.onNotification('fireCodeLensRefresh', () => { void connection.sendRequest(CodeLensRefreshRequest.type) }) connection.onNotification('fireSemanticTokensRefresh', () => { void connection.sendRequest(SemanticTokensRefreshRequest.type) }) connection.onNotification('fireInlayHintsRefresh', () => { void connection.sendRequest(InlayHintRefreshRequest.type) }) connection.onCodeLensResolve(codelens => { return {range: codelens.range, command: {title: 'format', command: 'editor.action.format'}} }) connection.onDeclaration(params => { assert.equal(params.position.line, 1) assert.equal(params.position.character, 1) return {uri: params.textDocument.uri, range: {start: {line: 1, character: 1}, end: {line: 1, character: 2}}} }) connection.onDefinition(params => { assert.equal(params.position.line, 1) assert.equal(params.position.character, 1) return {uri: params.textDocument.uri, range: {start: {line: 0, character: 0}, end: {line: 0, character: 1}}} }) connection.onHover(_params => { return { contents: { kind: MarkupKind.PlainText, value: 'foo' } } }) connection.onCompletion(_params => { return [ {label: 'item', insertText: 'text'} ] }) connection.onCompletionResolve(item => { item.detail = 'detail' return item }) connection.onSignatureHelp(_params => { const result = { signatures: [ SignatureInformation.create('label', 'doc', ParameterInformation.create('label', 'doc')) ], activeSignature: 1, activeParameter: 1 } return result }) connection.onReferences(params => { return [ Location.create(params.textDocument.uri, Range.create(0, 0, 0, 0)), Location.create(params.textDocument.uri, Range.create(1, 1, 1, 1)) ] }) connection.onDocumentHighlight(_params => { return [ DocumentHighlight.create(Range.create(2, 2, 2, 2), DocumentHighlightKind.Read) ] }) connection.onCodeAction(params => { if (params.textDocument.uri.endsWith('empty.bat')) return undefined return [ CodeAction.create('title', Command.create('title', 'test_command')), CodeAction.create('other title'), Command.create('title', 'test_command') ] }) connection.onExecuteCommand(params => { if (params.command == 'test_command') { return {success: true} } }) connection.onCodeActionResolve(codeAction => { codeAction.title = 'resolved' return codeAction }) connection.onDocumentFormatting(_params => { return [ TextEdit.insert(Position.create(0, 0), 'insert') ] }) connection.onDocumentRangeFormatting(_params => { return [ TextEdit.del(Range.create(1, 1, 1, 2)) ] }) connection.onDocumentOnTypeFormatting(_params => { return [ TextEdit.replace(Range.create(2, 2, 2, 3), 'replace') ] }) connection.onPrepareRename(_params => { return Range.create(1, 1, 1, 2) }) connection.onRenameRequest(_params => { return {documentChanges: []} }) connection.onDocumentLinks(_params => { return [ DocumentLink.create(Range.create(1, 1, 1, 2)) ] }) connection.onDocumentLinkResolve(link => { link.target = URI.file('/target.txt').toString() return link }) connection.onDocumentSymbol(_params => { return [ DocumentSymbol.create('name', undefined, SymbolKind.Method, Range.create(1, 1, 3, 1), Range.create(2, 1, 2, 3)) ] }) connection.onDocumentColor(_params => { return [ ColorInformation.create(Range.create(1, 1, 1, 2), Color.create(1, 1, 1, 1)) ] }) connection.onColorPresentation(_params => { return [ ColorPresentation.create('label') ] }) connection.onFoldingRanges(_params => { return [ FoldingRange.create(1, 2) ] }) connection.onImplementation(params => { assert.equal(params.position.line, 1) assert.equal(params.position.character, 1) return {uri: params.textDocument.uri, range: {start: {line: 2, character: 2}, end: {line: 3, character: 3}}} }) connection.onSelectionRanges(_params => { return [ SelectionRange.create(Range.create(1, 2, 3, 4)) ] }) let lastFileOperationRequest connection.workspace.onDidCreateFiles(params => {lastFileOperationRequest = {type: 'create', params}}) connection.workspace.onDidRenameFiles(params => {lastFileOperationRequest = {type: 'rename', params}}) connection.workspace.onDidDeleteFiles(params => {lastFileOperationRequest = {type: 'delete', params}}) connection.onRequest( new ProtocolRequestType('testing/lastFileOperationRequest'), () => { return lastFileOperationRequest }, ) connection.workspace.onWillCreateFiles(params => { const createdFilenames = params.files.map(f => `${f.uri}`).join('\n') return { documentChanges: [{ textDocument: {uri: '/dummy-edit', version: null}, edits: [ TextEdit.insert(Position.create(0, 0), `WILL CREATE:\n${createdFilenames}`), ] }], } }) connection.workspace.onWillRenameFiles(params => { const renamedFilenames = params.files.map(f => `${f.oldUri} -> ${f.newUri}`).join('\n') return { documentChanges: [{ textDocument: {uri: '/dummy-edit', version: null}, edits: [ TextEdit.insert(Position.create(0, 0), `WILL RENAME:\n${renamedFilenames}`), ] }], } }) connection.workspace.onWillDeleteFiles(params => { const deletedFilenames = params.files.map(f => `${f.uri}`).join('\n') return { documentChanges: [{ textDocument: {uri: '/dummy-edit', version: null}, edits: [ TextEdit.insert(Position.create(0, 0), `WILL DELETE:\n${deletedFilenames}`), ] }], } }) connection.onTypeDefinition(params => { assert.equal(params.position.line, 1) assert.equal(params.position.character, 1) return {uri: params.textDocument.uri, range: {start: {line: 2, character: 2}, end: {line: 3, character: 3}}} }) connection.languages.callHierarchy.onPrepare(params => { return [ { kind: SymbolKind.Function, name: 'name', range: Range.create(1, 1, 1, 1), selectionRange: Range.create(2, 2, 2, 2), uri: params.textDocument.uri } ] }) connection.languages.callHierarchy.onIncomingCalls(params => { return [ { from: params.item, fromRanges: [Range.create(1, 1, 1, 1)] } ] }) connection.languages.callHierarchy.onOutgoingCalls(params => { return [ { to: params.item, fromRanges: [Range.create(1, 1, 1, 1)] } ] }) connection.languages.semanticTokens.onRange(() => { return { resultId: '1', data: [] } }) connection.languages.semanticTokens.on(() => { return { resultId: '2', data: [] } }) connection.languages.semanticTokens.onDelta(() => { return { resultId: '3', data: [] } }) connection.languages.diagnostics.on(() => { return { kind: DocumentDiagnosticReportKind.Full, items: [ Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) ] } }) connection.languages.diagnostics.onWorkspace(() => { return { items: [{ kind: DocumentDiagnosticReportKind.Full, uri: 'uri', version: 1, items: [ Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) ] }] } }) const typeHierarchySample = { superTypes: [], subTypes: [] } connection.languages.typeHierarchy.onPrepare(params => { const currentItem = { kind: SymbolKind.Class, name: 'ClazzB', range: Range.create(1, 1, 1, 1), selectionRange: Range.create(2, 2, 2, 2), uri: params.textDocument.uri } typeHierarchySample.superTypes = [{...currentItem, name: 'classA', uri: 'uri-for-A'}] typeHierarchySample.subTypes = [{...currentItem, name: 'classC', uri: 'uri-for-C'}] return [currentItem] }) connection.languages.typeHierarchy.onSupertypes(_params => { return typeHierarchySample.superTypes }) connection.languages.typeHierarchy.onSubtypes(_params => { return typeHierarchySample.subTypes }) connection.languages.inlineValue.on(_params => { return [ InlineValueText.create(Range.create(1, 2, 3, 4), 'text'), InlineValueVariableLookup.create(Range.create(1, 2, 3, 4), 'variableName', false), InlineValueEvaluatableExpression.create(Range.create(1, 2, 3, 4), 'expression'), ] }) connection.languages.inlayHint.on(() => { const one = InlayHint.create(Position.create(1, 1), [InlayHintLabelPart.create('type')], InlayHintKind.Type) one.data = '1' const two = InlayHint.create(Position.create(2, 2), [InlayHintLabelPart.create('parameter')], InlayHintKind.Parameter) two.data = '2' return [one, two] }) connection.languages.inlayHint.resolve(hint => { if (typeof hint.label === 'string') { hint.label = 'tooltip' } else { hint.label[0].tooltip = 'tooltip' } return hint }) connection.languages.onLinkedEditingRange(() => { return { ranges: [Range.create(1, 1, 1, 1)], wordPattern: '\\w' } }) connection.languages.inlineCompletion.on(_params => { return [ InlineCompletionItem.create('text inline', 'te', Range.create(1, 2, 3, 4)) ] }) connection.workspace.textDocumentContent.on(_params => { return {text: 'Some test content'} }) connection.onRequest( new ProtocolRequestType('testing/sendSampleProgress'), async (_, __) => { const progressToken = 'TEST-PROGRESS-TOKEN' await connection.sendRequest(WorkDoneProgressCreateRequest.type, {token: progressToken}) void connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'begin', title: 'Test Progress'}) void connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'report', percentage: 50, message: 'Halfway!'}) void connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'end', message: 'Completed!'}) }, ) connection.onRequest( new ProtocolRequestType('testing/beginOnlyProgress'), async (_, __) => { const progressToken = 'TEST-PROGRESS-BEGIN' await connection.sendRequest(WorkDoneProgressCreateRequest.type, {token: progressToken}) }, ) connection.onRequest(new ProtocolRequestType('testing/sendPercentageProgress'), async (_, __) => { // According to the spec, the reported percentage has to be an integer. // Because JS doesn't have integer support, we have rounding code in place. const progressToken2 = 'TEST-PROGRESS-PERCENTAGE' await connection.sendRequest(WorkDoneProgressCreateRequest.type, {token: progressToken2}) const progress = connection.window.attachWorkDoneProgress(progressToken2) progress.begin('Test Progress', 0.1) progress.report(49.9, 'Halfway!') progress.done() }) const uri = 'file:///abc.txt' connection.onWorkspaceSymbol(() => { return [ {name: 'name', kind: SymbolKind.Array, location: {uri}} ] }) connection.onWorkspaceSymbolResolve(symbol => { symbol.location = Location.create(symbol.location.uri, Range.create(1, 2, 3, 4)) return symbol }) connection.onRequest(new ProtocolRequestType('testing/sendApplyEdit'), async (_, __) => { const params = {label: 'Apply Edit', edit: {}} await connection.sendRequest(ApplyWorkspaceEditRequest.type, params) }) connection.onRequest(new ProtocolRequestType('testing/sendDiagnostics'), async (_, __) => { const diagnostics = [{ severity: DiagnosticSeverity.Warning, range: { start: {line: 0, character: 0}, end: {line: 0, character: 5} }, message: "Example warning: Check your code!", source: "ex" }] connection.sendDiagnostics({uri, diagnostics}) }) // Listen on the connection connection.listen() ================================================ FILE: src/__tests__/client/server/timeoutOnShutdownServer.js ================================================ "use strict" Object.defineProperty(exports, "__esModule", {value: true}) const node_1 = require("vscode-languageserver/node") const connection = (0, node_1.createConnection)() connection.onInitialize((_params) => { return {capabilities: {}} }) connection.onShutdown(async () => { return new Promise((resolve) => { setTimeout(resolve, 200000) }) }) connection.listen() ================================================ FILE: src/__tests__/client/textSynchronization.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuidv4 } from 'uuid' import { DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, DocumentSelector, Position, Range, TextDocumentSaveReason, TextEdit, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import { LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from '../../language-client/index' import Document from '../../model/document' import { TextDocumentContentChange } from '../../types' import { remove } from '../../util/fs' import workspace from '../../workspace' import helper from '../helper' function createClient(documentSelector: DocumentSelector | undefined | null | LanguageClientOptions, middleware: Middleware = {}, opts: any = {}): LanguageClient { const serverModule = path.join(__dirname, './server/testDocuments.js') const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } } if (documentSelector === undefined) documentSelector = [{ scheme: 'file' }] const clientOptions: LanguageClientOptions = { documentSelector: Array.isArray(documentSelector) ? documentSelector : undefined, synchronize: {}, initializationOptions: opts, middleware }; (clientOptions as ({ $testMode?: boolean })).$testMode = true if (documentSelector && !Array.isArray(documentSelector)) Object.assign(clientOptions, documentSelector) const result = new LanguageClient('test', 'Test Language Server', serverOptions, clientOptions) return result } let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = workspace.nvim }) afterEach(async () => { await helper.reset() }) afterAll(async () => { await helper.shutdown() }) async function loadBuffer(filepath: string): Promise { let nr = await nvim.call('bufadd', [filepath]) as number await nvim.call('bufload', [nr]) await helper.waitValue(async () => { return workspace.getDocument(nr) != null }, true) return workspace.getDocument(nr) } describe('TextDocumentSynchronization', () => { describe('DidOpenTextDocumentFeature', () => { it('should register with empty documentSelector', async () => { let client = createClient(undefined) await client.start() let feature = client.getFeature(DidOpenTextDocumentNotification.method) feature.register({ id: uuidv4(), registerOptions: { documentSelector: null } }) let res = await client.sendRequest('getLastOpen') expect(res).toBe(null) let docs = feature.openDocuments expect(docs).toBeDefined() await client.stop() }) it('should send event on document create', async () => { let client = createClient([{ language: 'vim' }]) await client.start() let uri = URI.file(path.join(os.tmpdir(), 't.vim')) let doc = await workspace.loadFile(uri.toString()) expect(doc.languageId).toBe('vim') let res = await client.sendRequest('getLastOpen') as any expect(res.uri).toBe(doc.uri) expect(res.version).toBe(doc.version) await client.stop() }) it('should work with middleware', async () => { let called = false let throwError = false let client = createClient({ documentSelector: [{ language: 'vim' }], textSynchronization: {} }, { didOpen: (doc, next) => { called = true if (throwError) throw new Error('myerror') return next(doc) } }) await client.start() let uri = URI.file(path.join(os.tmpdir(), 't.js')) let doc = await workspace.loadFile(uri.toString()) expect(doc.languageId).toBe('javascript') let feature = client.getFeature(DidOpenTextDocumentNotification.method) feature.register({ id: uuidv4(), registerOptions: { documentSelector: [{ language: 'javascript' }] } }) let res = await client.sendRequest('getLastOpen') as any expect(res.uri).toBe(doc.uri) expect(called).toBe(true) throwError = true uri = URI.file(path.join(os.tmpdir(), 'a.js')) await workspace.loadFile(uri.toString()) await client.stop() }) it('should delayOpenNotifications', async () => { let uri = URI.file(path.join(os.tmpdir(), 'x.vim')) await workspace.loadFile(uri.toString()) let loaded: Set = new Set() let throwError = false let client = createClient({ documentSelector: [{ language: 'vim' }], textSynchronization: { delayOpenNotifications: true } }, { didOpen: (data, next) => { loaded.add(URI.parse(data.uri).fsPath) if (throwError) return Promise.reject(new Error('my error')) return next(data) } }) await client.start() let feature = client.getFeature(DidOpenTextDocumentNotification.method) as any let filepath = path.join(os.tmpdir(), 't.vim') let doc = await loadBuffer(filepath) expect(loaded.has(filepath)).toBe(false) await nvim.command(`b ${doc.bufnr}`) await helper.waitValue(() => loaded.has(filepath), true) await nvim.command(`bwipeout`) filepath = path.join(os.tmpdir(), 'p.vim') doc = await loadBuffer(filepath) await feature.sendPendingOpenNotifications(doc.uri) expect(loaded.has(filepath)).toBe(false) await feature.callback(doc.textDocument) await feature.callback(TextDocument.create('untitled:///1', 'tex', 1, '')) await feature.sendPendingOpenNotifications() expect(loaded.has(filepath)).toBe(true) throwError = true filepath = path.join(os.tmpdir(), 'foo.vim') doc = await loadBuffer(filepath) feature._pendingOpenNotifications.set(doc.uri, doc.textDocument) await nvim.command(`b ${doc.bufnr}`) await helper.waitValue(() => loaded.has(filepath), true) await client.stop() }) }) describe('DidCloseTextDocumentFeature', () => { it('should send close event', async () => { let uri = URI.file(path.join(os.tmpdir(), 'close.vim')) let doc = await workspace.loadFile(uri.toString()) let client = createClient([{ language: 'vim' }]) await client.start() await workspace.nvim.command(`bd! ${doc.bufnr}`) await helper.wait(30) let res = await client.sendRequest('getLastClose') as any expect(res.uri).toBe(doc.uri) await client.stop() }) it('should unregister document selector', async () => { let called = false let client = createClient([{ language: 'javascript' }], { didClose: (e, next) => { called = true return next(e) } }) await client.start() let openFeature = client.getFeature(DidOpenTextDocumentNotification.method) let id = uuidv4() let options = { id, registerOptions: { documentSelector: [{ language: 'vim' }] } } openFeature.register(options) let feature = client.getFeature(DidCloseTextDocumentNotification.method) feature.register(options) let uri = URI.file(path.join(os.tmpdir(), 'close.vim')) await workspace.loadFile(uri.toString()) await helper.wait(10) feature.unregister('unknown') let spy = jest.spyOn(client, 'sendNotification').mockReturnValue(Promise.reject(new Error('myerror'))) feature.unregister(id) spy.mockRestore() let res = await client.sendRequest('getLastClose') as any expect(res).toBeNull() expect(called).toBe(true) await client.stop() }) }) describe('DidChangeTextDocumentFeature', () => { it('should send full change event ', async () => { let called = false let throwError = false let client = createClient([{ language: 'vim' }], { didChange: (e, next) => { called = true if (throwError) return Promise.reject(new Error('myerror')) return next(e) } }) await client.start() let uri = URI.file(path.join(os.tmpdir(), 'x.vim')) let doc = await workspace.loadFile(uri.toString()) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) let res = await client.sendRequest('getLastChange') as any expect(res.text).toBe('bar\n') expect(called).toBe(true) throwError = true await doc.applyEdits([TextEdit.replace(Range.create(0, 0, 0, 3), '')]) await client.stop() }) it('should send incremental change event', async () => { let client = createClient([{ scheme: 'lsptest' }]) expect(client.isSynced('untitled:///1')).toBe(false) await client.start() await client.sendNotification('registerDocumentSync') let feature = client.getFeature(DidChangeTextDocumentNotification.method) feature.register({ registerOptions: {} } as any) let textDocument = TextDocument.create('untitled:///1', 'x', 1, '') expect(feature.getProvider(textDocument)).toBeUndefined() let called = false feature.onNotificationSent(() => { called = true }) let doc = await helper.createDocument(`${uuidv4()}.vim`) await helper.waitValue(() => { return client.isSynced(doc.uri) }, true) await nvim.call('setline', [1, 'bar']) await doc.patchChange() await helper.waitValue(() => { return called }, true) let res = await client.sendRequest('getLastChange') as any expect(res.uri).toBe(doc.uri) expect(res.text).toBe('bar\n') let provider = feature.getProvider(doc.textDocument) expect(provider).toBeDefined() await provider.send({ contentChanges: [], textDocument: { uri: doc.uri, version: doc.version }, bufnr: doc.bufnr, original: '', document: doc.textDocument, originalLines: [] }) await client.sendNotification('unregisterDocumentSync') await client.stop() }) it('should not send change event when syncKind is none', async () => { let client = createClient([{ scheme: 'lsptest' }], {}, { none: true }) await client.start() await client.sendNotification('registerDocumentSync') await nvim.command('edit x.vim') let doc = await workspace.document let feature = client.getFeature(DidChangeTextDocumentNotification.method) await helper.waitValue(() => { return feature.getProvider(doc.textDocument) != null }, true) let provider = feature.getProvider(doc.textDocument) let changes: TextDocumentContentChange[] = [{ range: Range.create(0, 0, 0, 0), text: 'foo' }] await provider.send({ contentChanges: changes, document: TextDocument.create(doc.uri, doc.languageId, 2, ''), textDocument: { uri: doc.uri, version: doc.version }, bufnr: doc.bufnr } as any) let res = await client.sendRequest('getLastChange') as any expect(res.text).toBe('\n') await client.stop() }) }) describe('WillSaveFeature', () => { it('should will save event', async () => { let called = false let client = createClient([{ language: 'vim' }], { willSave: (e, next) => { called = true return next(e) } }) await client.start() let fsPath = path.join(os.tmpdir(), `${uuidv4()}.vim`) let uri = URI.file(fsPath) await workspace.openResource(uri.toString()) let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) let feature = client.getFeature(WillSaveTextDocumentNotification.method) let provider = feature.getProvider(doc.textDocument) expect(provider).toBeDefined() await provider.send({ document: doc.textDocument, bufnr: doc.bufnr, reason: TextDocumentSaveReason.Manual, waitUntil: () => {} }) let res = await client.sendRequest('getLastWillSave') as any expect(res.uri).toBe(doc.uri) await client.stop() expect(called).toBe(true) if (fs.existsSync(fsPath)) { fs.unlinkSync(fsPath) } }) }) describe('WillSaveWaitUntilFeature', () => { it('should send will save until request', async () => { let client = createClient([{ scheme: 'lsptest' }]) await client.start() await client.sendNotification('registerDocumentSync') let fsPath = path.join(os.tmpdir(), `${uuidv4()}-foo.vim`) let uri = URI.file(fsPath) await workspace.openResource(uri.toString()) let doc = await workspace.document let feature = client.getFeature(WillSaveTextDocumentNotification.method) feature.register({ registerOptions: {} } as any) await helper.waitValue(() => { return feature.getProvider(doc.textDocument) != null }, true) let waitFeature = client.getFeature(WillSaveTextDocumentWaitUntilRequest.method) waitFeature.register({ registerOptions: {} } as any) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'x')]) nvim.command('w', true) await helper.waitValue(() => { return doc.getDocumentContent() }, 'abcx\n') await client.sendNotification('unregisterDocumentSync') await client.stop() await remove(fsPath) }) it('should not throw on response error', async () => { let called = false let client = createClient([], { willSaveWaitUntil: (event, next) => { called = true return next(event) } }) await client.start() await client.sendNotification('registerDocumentSync') let fsPath = path.join(os.tmpdir(), `${uuidv4()}-error.vim`) let uri = URI.file(fsPath) await helper.waitValue(() => { let feature = client.getFeature(DidOpenTextDocumentNotification.method) let provider = feature.getProvider(TextDocument.create(uri.toString(), 'vim', 1, '')) return provider != null }, true) await workspace.openResource(uri.toString()) let doc = await workspace.document await doc.synchronize() nvim.command('w', true) await helper.waitValue(() => { return called }, true) await client.stop() }) it('should unregister event handler', async () => { let client = createClient(null) await client.start() await client.sendNotification('registerDocumentSync') await helper.waitValue(() => { let feature = client.getFeature(DidOpenTextDocumentNotification.method) let provider = feature.getProvider(TextDocument.create('file:///f.vim', 'vim', 1, '')) return provider != null }, true) await client.sendNotification('unregisterDocumentSync') await helper.waitValue(() => { let feature = client.getFeature(DidOpenTextDocumentNotification.method) let provider = feature.getProvider(TextDocument.create('file:///f.vim', 'vim', 1, '')) return provider == null }, true) await client.stop() }) }) describe('DidSaveTextDocumentFeature', () => { it('should send did save notification', async () => { let called = false let client = createClient([{ language: 'vim' }], { didSave: (e, next) => { called = true return next(e) } }) await client.start() let fsPath = path.join(os.tmpdir(), `${uuidv4()}.vim`) let uri = URI.file(fsPath) await workspace.openResource(uri.toString()) let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) nvim.command('w', true) await helper.waitValue(() => { return called }, true) let res = await client.sendRequest('getLastWillSave') as any expect(res.uri).toBe(doc.uri) await client.stop() fs.unlinkSync(fsPath) }) }) }) ================================================ FILE: src/__tests__/client/utils.test.ts ================================================ /* eslint-disable */ import assert from 'assert' import { spawn } from 'child_process' import { NotificationType, NotificationType1, RequestType, RequestType1 } from 'vscode-languageserver-protocol' import { checkProcessDied, handleChildProcessStartError } from '../../language-client/index' import { data2String, fixNotificationType, fixRequestType, getLocale, getParameterStructures, getTracePrefix, isValidNotificationType, isValidRequestType, parseTraceData } from '../../language-client/utils' import { Delayer } from '../../language-client/utils/async' import { CloseAction, DefaultErrorHandler, ErrorAction, toCloseHandlerResult } from '../../language-client/utils/errorHandler' import { ConsoleLogger, NullLogger } from '../../language-client/utils/logger' import { wait } from '../../util/index' import helper from '../helper' test('Logger', () => { const logger = new ConsoleLogger() logger.error('error') logger.warn('warn') logger.info('info') logger.log('log') const nullLogger = new NullLogger() nullLogger.error('error') nullLogger.warn('warn') nullLogger.info('info') nullLogger.log('log') }) test('checkProcessDied', async () => { checkProcessDied(undefined) let child = spawn('sleep', ['3'], { cwd: process.cwd(), detached: true }) checkProcessDied(child) await wait(20) assert.rejects(async () => { await handleChildProcessStartError(null, 'msg') }) }) test('getLocale', () => { process.env.LANG = '' expect(getLocale()).toBe('en') process.env.LANG = 'en_US.UTF-8' expect(getLocale()).toBe('en_US') }) test('getTraceMessage', () => { expect(getTracePrefix({})).toMatch('Trace') expect(getTracePrefix({ isLSPMessage: true, type: 'request' })).toMatch('LSP') }) test('getParameterStructures', () => { expect(getParameterStructures('auto').toString()).toBe('auto') // test all the cased of getParameterStructures expect(getParameterStructures('byPosition').toString()).toBe('byPosition') expect(getParameterStructures('byName').toString()).toBe('byName') expect(getParameterStructures('unknown').toString()).toBe('auto') }) test('isValidRequestType', () => { expect(isValidRequestType('test')).toBe(true) expect(isValidRequestType({ method: 'test' })).toBe(false) expect(isValidRequestType(new RequestType('test'))).toBe(true) }) test('isValidNotificationType', () => { expect(isValidNotificationType('test')).toBe(true) expect(isValidNotificationType({ method: 'test' })).toBe(false) expect(isValidNotificationType(new NotificationType('test'))).toBe(true) }) test('fixRequestType', () => { expect(fixRequestType('test', [])).toBe('test') for (let i = 0; i <= 10; i++) { let type = { method: 'test', numberOfParams: i } expect(fixRequestType(type, [])).toBeDefined() } let type = { method: 'test', numberOfParams: 1, parameterStructures: 'auto' } let res = fixRequestType(type, []) as RequestType1 expect(res.numberOfParams).toBe(1) expect(res.parameterStructures).toBeDefined() }) test('fixNotificationType', () => { expect(fixNotificationType('test', [])).toBe('test') for (let i = 0; i <= 10; i++) { let type = { method: 'test', numberOfParams: i } expect(fixNotificationType(type, [])).toBeDefined() } let type = { method: 'test', numberOfParams: 1, parameterStructures: 'auto' } let res = fixNotificationType(type, []) as NotificationType1 expect(res.numberOfParams).toBe(1) expect(res.parameterStructures).toBeDefined() }) test('data2String', () => { let err = new Error('my error') err.stack = undefined let text = data2String(err) expect(text).toMatch('error') }) test('parseTraceData', () => { expect(parseTraceData({})).toBe('{}') expect(parseTraceData('msg')).toMatch('msg') expect(parseTraceData('Params: data')).toMatch('data') expect(parseTraceData('Result: {"foo": "bar"}')).toMatch('bar') }) test('DefaultErrorHandler', async () => { let spy = jest.spyOn(console, 'error').mockImplementation(() => { // ignore }) let handler = new DefaultErrorHandler('test', 2) expect(handler.error(new Error('test'), { jsonrpc: '' }, 1).action).toBe(ErrorAction.Continue) expect(handler.error(new Error('test'), { jsonrpc: '' }, 5).action).toBe(ErrorAction.Shutdown) handler.closed() handler.milliseconds = 1 await wait(10) let res = handler.closed() expect(res.action).toBe(CloseAction.Restart) handler.milliseconds = 10 * 1000 res = handler.closed() expect(res.action).toBe(CloseAction.DoNotRestart) spy.mockRestore() expect(toCloseHandlerResult(CloseAction.DoNotRestart)).toBeDefined() handler = new DefaultErrorHandler('test', 1, helper.createNullChannel()) handler.closed() }) test('Delayer', () => { let count = 0 let factory = () => { return Promise.resolve(++count) } let delayer = new Delayer(0) let promises: Thenable[] = [] assert(!delayer.isTriggered()) delayer.trigger(factory, -1) promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })) assert(delayer.isTriggered()) promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })) assert(delayer.isTriggered()) promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })) assert(delayer.isTriggered()) return Promise.all(promises).then(() => { assert(!delayer.isTriggered()) }).finally(() => { delayer.dispose() }) }) test('Delayer - forceDelivery', async () => { let count = 0 let factory = () => { return Promise.resolve(++count) } let delayer = new Delayer(150) delayer.forceDelivery() delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) }) await wait(10) delayer.forceDelivery() expect(count).toBe(1) void delayer.trigger(factory) delayer.trigger(factory, -1) await wait(10) delayer.cancel() expect(count).toBe(1) }) test('Delayer - last task should be the one getting called', function() { let factoryFactory = (n: number) => () => { return Promise.resolve(n) } let delayer = new Delayer(0) let promises: Thenable[] = [] assert(!delayer.isTriggered()) promises.push(delayer.trigger(factoryFactory(1)).then((n) => { assert.equal(n, 3) })) promises.push(delayer.trigger(factoryFactory(2)).then((n) => { assert.equal(n, 3) })) promises.push(delayer.trigger(factoryFactory(3)).then((n) => { assert.equal(n, 3) })) const p = Promise.all(promises).then(() => { assert(!delayer.isTriggered()) }) assert(delayer.isTriggered()) return p }) ================================================ FILE: src/__tests__/client/workspaceFolder.test.ts ================================================ 'use strict' import * as assert from 'assert' import * as proto from 'vscode-languageserver-protocol' import { DidChangeWorkspaceFoldersParams, Disposable } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { BaseLanguageClient, MessageTransports } from '../../language-client/client' import { WorkspaceFoldersFeature } from '../../language-client/workspaceFolders' class TestLanguageClient extends BaseLanguageClient { protected createMessageTransports(): Promise { throw new Error('Method not implemented.') } public onRequest(): Disposable { return { dispose: () => {} } } } type MaybeFolders = proto.WorkspaceFolder[] | undefined class TestWorkspaceFoldersFeature extends WorkspaceFoldersFeature { public sendInitialEvent(currentWorkspaceFolders: MaybeFolders): void { super.sendInitialEvent(currentWorkspaceFolders) } public initializeWithFolders(currentWorkspaceFolders: MaybeFolders) { super.initializeWithFolders(currentWorkspaceFolders) } } function testEvent(initial: MaybeFolders, then: MaybeFolders, added: proto.WorkspaceFolder[], removed: proto.WorkspaceFolder[]) { const client = new TestLanguageClient('foo', 'bar', {}) let arg: any let spy = jest.spyOn(client, 'sendNotification').mockImplementation((_p1, p2) => { arg = p2 return Promise.resolve() }) const feature = new TestWorkspaceFoldersFeature(client) feature.initializeWithFolders(initial) feature.sendInitialEvent(then) expect(spy).toHaveBeenCalled() expect(spy).toHaveBeenCalledTimes(1) const notification: DidChangeWorkspaceFoldersParams = arg assert.deepEqual(notification.event.added, added) assert.deepEqual(notification.event.removed, removed) } function testNoEvent(initial: MaybeFolders, then: MaybeFolders) { const client = new TestLanguageClient('foo', 'bar', {}) let spy = jest.spyOn(client, 'sendNotification').mockImplementation(() => { return Promise.resolve() }) const feature = new TestWorkspaceFoldersFeature(client) feature.initializeWithFolders(initial) feature.sendInitialEvent(then) expect(spy).toHaveBeenCalledTimes(0) } describe('Workspace Folder Feature Tests', () => { const removedFolder = { uri: URI.parse('file://xox/removed').toString(), name: 'removedName', index: 0 } const addedFolder = { uri: URI.parse('file://foo/added').toString(), name: 'addedName', index: 0 } const addedProto = { uri: 'file://foo/added', name: 'addedName' } const removedProto = { uri: 'file://xox/removed', name: 'removedName' } test('remove/add', async () => { assert.ok(!MessageTransports.is({})) testEvent([removedFolder], [addedFolder], [addedProto], [removedProto]) }) test('remove', async () => { testEvent([removedFolder], [], [], [removedProto]) }) test('remove2', async () => { testEvent([removedFolder], undefined, [], [removedProto]) }) test('add', async () => { testEvent([], [addedFolder], [addedProto], []) }) test('add2', async () => { testEvent(undefined, [addedFolder], [addedProto], []) }) test('noChange1', async () => { testNoEvent([addedFolder, removedFolder], [addedFolder, removedFolder]) }) test('noChange2', async () => { testNoEvent([], []) }) test('noChange3', async () => { testNoEvent(undefined, undefined) }) }) ================================================ FILE: src/__tests__/coc-settings.json ================================================ { "suggest.timeout": 5000, "tslint.enable": false } ================================================ FILE: src/__tests__/completion/basic.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { v4 as uuidv4 } from 'uuid' import { CancellationToken, Disposable, Position, TextEdit } from 'vscode-languageserver-protocol' import commands from '../../commands' import completion, { Completion } from '../../completion' import sources from '../../completion/sources' import { CompleteOption, CompleteResult, ExtendedCompleteItem, ISource, SourceConfig, SourceType, VimCompleteItem } from '../../completion/types' import { WordDistance } from '../../completion/wordDistance' import events from '../../events' import { disposeAll, waitWithToken } from '../../util' import { byteLength } from '../../util/string' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() completion.loadConfiguration() }) function triggerCompletion(source: string): void { nvim.call('coc#start', { source }, true) } async function pumvisible(): Promise { let res = await nvim.call('coc#pum#visible', []) as number return res == 1 } async function create(items: string[] | VimCompleteItem[], trigger = true, conf?: Partial): Promise { let name = uuidv4() disposables.push(sources.createSource({ ...(conf ?? {}), name, doComplete: (_opt: CompleteOption): Promise> => new Promise(resolve => { if (items.length == 0 || typeof items[0] === 'string') { resolve({ items: items.map(s => { return { word: s } }) }) } else { resolve({ items: items as VimCompleteItem[] }) } }) })) let mode = await nvim.mode if (mode.mode !== 'i') { await nvim.command('startinsert') } if (trigger) { triggerCompletion(name) await helper.waitPopup() } return name } describe('completion', () => { describe('suggest configurations', () => { it('should select item by preselect', async () => { helper.updateConfiguration('suggest.noselect', true) expect(typeof Completion).toBe('function') await create([{ word: 'foo' }, { word: 'foo' }, { word: 'bar', preselect: true }], true) expect(events.completing).toBe(true) await nvim.input('br') await helper.waitValue(() => completion.activeItems.length, 1) await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'bar') }) it('should disable preselect feature', async () => { helper.updateConfiguration('suggest.enablePreselect', false) await create([{ word: 'foo' }, { word: 'bar' }, { word: 'foot', preselect: true }], true) let info = await nvim.call('coc#pum#info') as any expect(info.index).toBe(0) }) it('should trigger with none ascii characters', async () => { helper.updateConfiguration('suggest.asciiCharactersOnly', false) await create(['你好'], false) await nvim.input('ni') await helper.waitPopup() }, 10000) it('should use insert range instead of replace', async () => { helper.updateConfiguration('suggest.insertMode', 'insert') await helper.createDocument() await nvim.setLine('ffoo') let name = await create(['foo'], false) await nvim.call('cursor', [1, 2]) expect(sources.has(name)).toBe(true) await commands.executeCommand('editor.action.triggerSuggest', name) await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foofoo') }, 10000) it('should use ascii match', async () => { helper.updateConfiguration('suggest.asciiMatch', true) await create(['\xc1\xc7\xc8'], false) await nvim.input('a') await helper.waitPopup() let items = await helper.items() expect(items[0].word).toBe('ÁÇÈ') }, 10000) it('should not use ascii match', async () => { helper.updateConfiguration('suggest.asciiMatch', false) await create(['\xc1\xc7\xc8', 'foo'], false) await nvim.input('a') await helper.wait(50) let visible = await pumvisible() expect(visible).toBe(false) await nvim.input('') await nvim.input('f') await helper.waitPopup() }, 10000) it('should not trigger with none ascii characters', async () => { helper.updateConfiguration('suggest.asciiCharactersOnly', true) await create(['你好'], false) await nvim.input('你') await helper.wait(10) let visible = await pumvisible() expect(visible).toBe(false) }) it('should not trigger with number input', async () => { helper.updateConfiguration('suggest.ignoreRegexps', ['[0-9]+']) await create(['1234', '1984'], false) await nvim.input('1') await helper.waitFor('getline', ['.'], '1') let visible = await pumvisible() expect(visible).toBe(false) }) it('should disable filter on backspace', async () => { helper.updateConfiguration('suggest.filterOnBackspace', false) await create(['this', 'thoit'], true) await nvim.input('this') await helper.waitValue(() => { return completion.activeItems.length }, 1) await nvim.input('') await helper.waitValue(() => { return completion.isActivated }, false) }) it('should select recent used item', async () => { helper.updateConfiguration('suggest.selection', 'recentlyUsed') let name = await create(['foo', 'bar', 'foobar']) await helper.confirmCompletion(1) await nvim.input('f') triggerCompletion(name) let info = await nvim.call('coc#pum#info') as any expect(info.index).toBe(1) }) it('should not resolve timeout sources', async () => { helper.updateConfiguration('suggest.timeout', 30) disposables.push(sources.createSource({ name: 'timeout', doComplete: (_opt: CompleteOption, token) => new Promise(resolve => { let timer = setTimeout(() => { resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) }, 200) token.onCancellationRequested(() => { clearTimeout(timer) }) }) })) await nvim.input('if') await helper.waitFor('eval', ["get(g:,'coc_timeout_sources','')"], ['timeout']) }) it('should change default sort method', async () => { const assertWords = async (arr: string[]) => { await helper.waitPopup() let win = await helper.getFloat('pum') let words = await win.getVar('words') expect(words).toEqual(arr) } helper.updateConfiguration('suggest.defaultSortMethod', 'none') await create([{ word: 'far' }, { word: 'foobar' }, { word: 'foo' }], false) await nvim.input('f') await assertWords(['far', 'foobar', 'foo']) await nvim.input('') helper.updateConfiguration('suggest.defaultSortMethod', 'alphabetical') await helper.wait(10) await nvim.input('of') await assertWords(['far', 'foo', 'foobar']) }, 10000) it('should remove duplicated words', async () => { helper.updateConfiguration('suggest.removeDuplicateItems', true) await create([{ word: 'foo', dup: 1 }, { word: 'foo', dup: 1 }], true) let win = await helper.getFloat('pum') let words = await win.getVar('words') expect(words).toEqual(['foo']) }) it('should remove current word', async () => { helper.updateConfiguration('suggest.removeCurrentWord', true) let buf = await nvim.buffer let doc = workspace.getDocument(buf.id) await buf.setLines(['foo bar', ''], { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() await nvim.call('cursor', [2, 1]) await nvim.input('if') await helper.waitPopup() await nvim.input('oo') await helper.waitFor('coc#pum#visible', [], 0) }, 10000) it('should use border with floatConfig', async () => { let dispose = helper.updateConfiguration('suggest.floatConfig', { border: true, rounded: true, borderhighlight: 'Normal', title: 'title' }) await create([{ word: 'foo', kind: 'w', menu: 'x' }, { word: 'foobar', kind: 'w', menu: 'y' }], true) await helper.waitPopup() let win = await helper.getFloat('pum') let id = await nvim.call('coc#float#get_related', [win.id, 'border']) expect(id).toBeGreaterThan(1000) dispose() }, 10000) it('should use pumFloatConfig', async () => { helper.updateConfiguration('suggest.floatConfig', {}) helper.updateConfiguration('suggest.pumFloatConfig', { border: true, highlight: 'Normal', winblend: 15, shadow: true, rounded: true, title: 'suggest' }) await create([{ word: 'foo', kind: 'w', menu: 'x' }, { word: 'foobar', kind: 'w', menu: 'y' }], true) let win = await helper.getFloat('pum') let id = await nvim.call('coc#float#get_related', [win.id, 'border']) as number expect(id).toBeGreaterThan(1000) let hl = await win.getOption('winhl') expect(hl).toMatch('Normal') let border = nvim.createWindow(id) let buf = await border.buffer let lines = await buf.lines expect(lines[0]).toMatch('suggest') }) it('should do filter when autoTrigger is none', async () => { helper.updateConfiguration('suggest.autoTrigger', 'none') let doc = await workspace.document expect(completion.shouldTrigger(doc, '')).toBe(false) await create(['foo', 'bar'], false) await nvim.input('f') await helper.wait(10) expect(completion.activeItems.length).toBe(0) nvim.call('coc#start', [], true) await helper.waitPopup() expect(completion.activeItems.length).toBe(1) await nvim.input('o') await helper.wait(10) expect(completion.activeItems.length).toBe(1) }, 10000) it('should trigger for trigger character when filter failed', async () => { await nvim.command('edit tmp') let doc = await workspace.document doc.chars.addKeyword('-') let option: CompleteOption let source: ISource = { name: 'dash', enable: true, sourceType: SourceType.Service, triggerCharacters: ['-'], doComplete: async (opt: CompleteOption) => { option = opt if (opt.triggerCharacter == '-') return { items: [{ word: '-foo' }] } return { items: [{ word: 'foo' }, { word: 'bar' }, { label: undefined }] } } } disposables.push(sources.addSource(source)) await nvim.input('i') triggerCompletion('dash') await helper.waitPopup() expect(option.triggerCharacter).toBeUndefined() await nvim.input('-') await helper.waitValue(() => { let items = completion.activeItems return items && items.length == 1 && items[0].word == '-foo' }, true) }, 10000) it('should trigger on trigger character', async () => { helper.updateConfiguration('suggest.autoTrigger', 'none') let fn = jest.fn() let source: ISource = { name: 'trigger', enable: true, sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: (_opt: CompleteOption) => new Promise(resolve => { fn() resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) }) } disposables.push(sources.addSource(source)) await nvim.input('if.') await helper.wait(20) expect(fn).toHaveBeenCalledTimes(0) helper.updateConfiguration('suggest.autoTrigger', 'trigger') await nvim.input('f') await helper.wait(20) await nvim.input('.') await helper.waitPopup() }, 10000) it('should disable localityBonus', async () => { helper.updateConfiguration('suggest.localityBonus', false) let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), '\nfoo\nfoobar')]) await create(['foo', 'foobar'], true) await helper.confirmCompletion(0) }) it('should not show preview window when enableFloat is disabled', async () => { helper.updateConfiguration('suggest.enableFloat', false) let resolved = false disposables.push(sources.createSource({ name: 'info', doComplete: () => Promise.resolve({ items: [{ word: 'foo', info: 'detail' }] }), onCompleteResolve: () => { resolved = true } })) await nvim.command('startinsert') triggerCompletion('info') await helper.waitPopup() let floatWin = await helper.getFloat('pumdetail') expect(floatWin).toBeUndefined() await helper.confirmCompletion(0) await helper.waitValue(() => { return resolved }, true) }, 10000) it('should disable graceful filter', async () => { helper.updateConfiguration('suggest.filterGraceful', false) await create(['this'], true) await nvim.input('tih') await helper.waitValue(async () => { let items = await helper.items() return items.length }, 0) }) it('should change detailField', async () => { helper.updateConfiguration('suggest.detailField', 'abbr') await create([{ word: 'this', detail: 'detail of this' }], true) let floatWin = await helper.getFloat('pum') let buf = await floatWin.buffer expect(buf).toBeDefined() }, 10000) it('should change triggerCompletionWait', async () => { let doc = await workspace.document helper.updateConfiguration('suggest.triggerCompletionWait', 200) let name = await create([{ word: 'foo' }, { word: 'bar' }], false) triggerCompletion(name) let spy let p = new Promise(resolve => { spy = jest.spyOn(doc, 'patchChange').mockImplementation(() => { resolve() return Promise.resolve() }) }) await p await helper.wait(20) completion.cancelAndClose() spy.mockRestore() }) }) describe('suggest variables', () => { beforeEach(() => { disposables.push(sources.createSource({ name: 'foo', doComplete: (_opt: CompleteOption) => Promise.resolve({ items: [{ word: 'foo' }] }) })) }) it('should be disabled by b:coc_suggest_disable', async () => { let doc = await workspace.document await doc.buffer.setVar('coc_suggest_disable', 1) await nvim.input('if') await helper.wait(20) let visible = await pumvisible() expect(visible).toBe(false) }) it('should be disabled by b:coc_disabled_sources', async () => { let doc = await workspace.document await doc.buffer.setVar('coc_disabled_sources', ['foo']) await nvim.input('if') await helper.wait(20) let visible = await pumvisible() expect(visible).toBe(false) }) it('should be disabled by b:coc_suggest_blacklist', async () => { let doc = await workspace.document await doc.buffer.setVar('coc_suggest_blacklist', ['end']) await nvim.setLine('en') await nvim.input('Ad') await helper.wait(10) let visible = await pumvisible() expect(visible).toBe(false) }) }) describe('shouldComplete()', () => { it('should not complete when shouldComplete return false', async () => { let name = Math.random().toString(16).slice(-6) let called = false let shouldRun = false disposables.push(sources.addSource({ name, shouldComplete: () => { return shouldRun }, doComplete: (_opt: CompleteOption): Promise> => new Promise(resolve => { called = true resolve({ items: [{ word: 'foo' }] }) }) })) await nvim.input('i') triggerCompletion(name) await helper.wait(20) expect(called).toBe(false) shouldRun = true triggerCompletion(name) await helper.waitPopup() }, 10000) it('should not complete with empty sources', async () => { nvim.call('coc#start', { source: 'not_exists' }, true) await helper.wait(10) let visible = await pumvisible() expect(visible).toBe(false) }) }) describe('doComplete()', () => { it('should create pum', async () => { let source: ISource = { enable: true, name: 'menu', shortcut: '', sourceType: SourceType.Service, doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'foo', deprecated: true, menu: 'm', kind: 'k' }] }) }) } disposables.push(sources.addSource(source)) disposables.push(sources.addSource({ enable: true, name: 'other', shortcut: 's', sourceType: SourceType.Service, doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'bar', menu: '' }] }) }) })) await nvim.input('i') await nvim.call('coc#start', {}) await helper.waitPopup() let info = await nvim.call('coc#pum#info') as any expect(info.index).toBe(0) }, 10000) it('should show slow source', async () => { let source: ISource = { priority: 0, enable: true, name: 'slow', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: (_opt: CompleteOption) => new Promise(resolve => { setTimeout(() => { resolve({ items: [{ word: 'foo', kind: 'w' }, { word: 'bar' }] }) }, 50) }) } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.waitPopup() expect(completion.isActivated).toBe(true) let items = await helper.items() expect(items.length).toBe(2) await nvim.input('foo') await helper.wait(50) items = await helper.items() expect(items.length).toBe(1) }, 10000) it('should catch error', async () => { disposables.push(sources.createSource({ name: 'error', doComplete: (_opt: CompleteOption) => new Promise((_resolve, reject) => { reject(new Error('custom error')) }) })) await nvim.input('if') await helper.wait(50) let cmdline = await helper.getCmdline() expect(cmdline).toMatch('') }) it('should show items before slow source finished', async () => { let source: ISource = { name: 'fast', enable: true, doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) }) } disposables.push(sources.addSource(source)) let finished = false let slowSource: ISource = { name: 'slow', enable: true, doComplete: (_opt: CompleteOption, token) => new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) resolve(undefined) }) let timer = setTimeout(() => { finished = true resolve({ items: [{ word: 'world' }] }) }, 300) }) } disposables.push(sources.addSource(slowSource)) await nvim.input('if') await events.race(['MenuPopupChanged'], 200) expect(finished).toBe(false) }) it('should show items when wordDistance is slow', async () => { let _resolve let spy = jest.spyOn(WordDistance, 'create').mockImplementation(() => { return new Promise(resolve => { _resolve = resolve }) }) await create(['foo', 'foot'], false) await nvim.input('f') await helper.waitPopup() _resolve(undefined) spy.mockRestore() }, 10000) }) describe('resumeCompletion()', () => { it('should not cancel when trigger for inComplete', async () => { let name = Math.random().toString(16).slice(-6) let _resolve let fireResolve = () => { _resolve({ items: [{ word: 'foo' }, { word: 'foot' }] }) } disposables.push(sources.createSource({ name, doComplete: (_opt: CompleteOption) => new Promise(resolve => { _resolve = resolve }) })) disposables.push(sources.createSource({ name: 'inComplete', doComplete: (opt: CompleteOption) => new Promise(resolve => { if (opt.input.length == 1) { resolve({ items: [{ word: 'fa' }], isIncomplete: true }) } else { resolve({ items: [{ word: 'footman' }, { word: 'football' }, { word: 'fa' }], isIncomplete: false }) } }) })) await nvim.input('if') await helper.waitPopup() let items = completion.activeItems expect(items.length).toBe(1) await nvim.input('o') await helper.wait(30) fireResolve() await helper.waitValue(() => { return completion.activeItems.length }, 4) }, 10000) it('should refresh pum when complete inComplete sources', async () => { let name = Math.random().toString(16).slice(-6) disposables.push(sources.createSource({ name, doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'foo' }, { word: 'foot' }] }) }) })) let timer let called = false disposables.push(sources.createSource({ name: 'inComplete', doComplete: (opt: CompleteOption, token) => new Promise(resolve => { if (opt.input.length == 1) { resolve({ items: [{ word: 'fa' }], isIncomplete: true }) } else { token.onCancellationRequested(() => { called = true }) timer = setTimeout(() => { resolve({ items: [{ word: 'footman' }, { word: 'football' }, { word: 'fa' }], isIncomplete: false }) }, 1000) } }) })) await nvim.input('if') await helper.waitPopup() await nvim.input('t') await helper.waitValue((() => { let activeItems = completion.activeItems return activeItems.length == 1 && activeItems[0].word === 'foot' }), true) await nvim.input('t') await helper.waitValue(() => called, true) clearTimeout(timer) }, 10000) it('should stop if no filtered items', async () => { await create(['foo', 'bar'], true) expect(completion.isActivated).toBe(true) await nvim.input('fp') await helper.waitValue(() => { return completion.isActivated }, false) }) it('should stop when selected and no filtered items', async () => { helper.updateConfiguration('suggest.noselect', true) await create(['foo'], true) expect(completion.isActivated).toBe(true) await nvim.call('coc#pum#_navigate', [1, 1]) await helper.waitFor('getline', ['.'], 'foo') await nvim.input('(') await helper.waitValue(() => { return completion.isActivated }, false) }) it('should not resume after text change', async () => { await create(['foo'], false) await nvim.input('f') await helper.waitPopup() await nvim.setLine('fo') await nvim.call('cursor', [2, 3]) await helper.waitValue(() => { return completion.isActivated }, false) }, 10000) it('should stop with bad insert on CursorMovedI', async () => { await create(['foo', 'fat'], false) await nvim.input('f') await nvim.setLine('f a') await nvim.call('cursor', [2, 4]) await helper.wait(30) let visible = await pumvisible() expect(visible).toBe(false) }) it('should deactivate without filtered items', async () => { await create(['foo', 'foobar'], true) await nvim.input('f') await nvim.input(' a') await helper.waitFor('coc#pum#visible', [], 0) expect(completion.isActivated).toBe(false) completion.cancel() }) it('should deactivate when insert space', async () => { let source: ISource = { priority: 0, enable: true, name: 'empty', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'foo bar' }] }) }) } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.waitPopup() expect(completion.isActivated).toBe(true) let items = await helper.items() expect(items[0].word).toBe('foo bar') await nvim.input(' ') await helper.waitValue(async () => { return await pumvisible() }, false) }, 10000) it('should use resume input to filter', async () => { let source: ISource = { priority: 0, enable: true, name: 'source', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: () => new Promise(resolve => { setTimeout(() => { resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) }, 60) }) } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.wait(20) await nvim.input('f') await helper.waitPopup() expect(completion.isActivated).toBe(true) let items = await helper.items() expect(items.length).toBe(1) expect(items[0].word).toBe('foo') }, 10000) it('should filter slow source', async () => { disposables.push(sources.addSource({ name: 'fast', enable: true, shortcut: 's', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'xyz', menu: '' }] }) }) })) let source: ISource = { priority: 0, enable: true, name: 'slow', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: () => new Promise(resolve => { setTimeout(() => { resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) }, 100) }) } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.wait(10) await nvim.input('f') await helper.waitPopup() await nvim.input('o') await helper.waitValue((() => { return completion.activeItems?.length }), 1) }, 10000) it('should complete inComplete source', async () => { let source: ISource = { priority: 0, enable: true, name: 'inComplete', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: async (opt: CompleteOption) => { if (opt.input.length <= 1) { return { isIncomplete: true, items: [{ word: 'foo' }, { word: opt.input }] } } await helper.wait(10) return { isIncomplete: false, items: [{ word: 'foo' }, { word: opt.input }] } } } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.waitPopup() expect(completion.isActivated).toBe(true) await nvim.input('a') await helper.wait(20) await nvim.input('b') }, 10000) it('should not complete inComplete source when isIncomplete is false', async () => { let source: ISource = { priority: 0, enable: true, name: 'inComplete', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: async (opt: CompleteOption) => { await helper.wait(30) if (opt.input.length <= 1) { return { isIncomplete: true, items: [{ word: 'foobar' }] } } return { isIncomplete: false, items: [{ word: 'foobar' }] } } } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.waitPopup() expect(completion.isActivated).toBe(true) await nvim.input('fo') await helper.wait(50) await nvim.input('b') await helper.wait(50) expect(completion.isActivated).toBe(true) }, 10000) it('should filter when type character after item selected without handle complete done', async () => { let input: string let fn = jest.fn() let source: ISource = { priority: 0, enable: true, name: 'filter', sourceType: SourceType.Service, doComplete: opt => { input = opt.input if (input == 'f') return Promise.resolve({ items: [{ word: 'fo' }] }) if (input == 'foo') return Promise.resolve({ items: [{ word: 'foobar' }, { word: 'foot' }] }) return Promise.resolve({ items: [] }) }, onCompleteDone: () => { fn() } } disposables.push(sources.addSource(source)) await nvim.input('if') await helper.waitPopup() await nvim.call('coc#pum#_navigate', [1, 1]) await helper.wait(20) await nvim.input('o') await helper.waitPopup() expect(fn).toHaveBeenCalledTimes(0) }, 10000) }) describe('TextChangedI', () => { it('should filter on backspace', async () => { await create(['foo', 'fbi'], true) await nvim.input('fo') await helper.waitValue(() => completion.activeItems.length, 1) await nvim.input('') await helper.waitValue(() => completion.activeItems.length, 2) }) it('should respect commit character', async () => { helper.updateConfiguration('suggest.acceptSuggestionOnCommitCharacter', true) let source: ISource = { enable: true, name: 'commit', sourceType: SourceType.Service, triggerCharacters: ['.'], doComplete: (opt: CompleteOption) => { if (opt.triggerCharacter == '.') { return Promise.resolve({ items: [{ word: 'bar' }] }) } return Promise.resolve({ items: [{ word: 'foo' }] }) }, shouldCommit: (_item, character) => character == '.' } disposables.push(sources.addSource(source)) await nvim.input('if') await helper.waitPopup() await nvim.input('o.') await helper.waitFor('getline', ['.'], 'foo.') }, 10000) it('should cancel on CursorMoved', async () => { await nvim.setLine('first line') await nvim.input('o') await create(['foo', 'foot']) let [_, line, col] = await nvim.call('getcurpos') as number[] completion.onCursorMovedI(events.bufnr, [line, col], false) expect(completion.isActivated).toBe(true) completion.onCursorMovedI(events.bufnr, [line, col - 1], false) expect(completion.isActivated).toBe(false) await events.fire('PumNavigate', []) }) it('should stop completion with invalid input', async () => { await nvim.setLine('line ') await nvim.input('Af') await create(['foo', 'foot']) await nvim.setLine('abcd f') await helper.waitValue(() => completion.isActivated, false) await completion.filterResults() }) it('should check indent change', async () => { await create(['foo', 'bar']) const linenr = completion.option.linenr let changed = completion.hasIndentChange({ lnum: linenr + 1, col: 1, line: '', changedtick: 0, pre: '', }) expect(changed).toBe(false) }) }) describe('TextChangedP', () => { it('should cancel on CursorMoved', async () => { let buf = await nvim.buffer await buf.setLines(['', 'bar'], { start: 0, end: -1, strictIndexing: false }) let source: ISource = { priority: 99, enable: true, name: 'temp', sourceType: SourceType.Service, doComplete: (_opt: CompleteOption) => Promise.resolve({ items: [{ word: 'foo#abc' }] }), } disposables.push(sources.addSource(source)) await nvim.input('if') await helper.waitPopup() void events.fire('CompleteDone', [{}]) await helper.wait(10) await events.fire('CursorMovedI', [buf.id, [2, 1, '']]) await helper.waitValue(() => { return completion.isActivated }, false) }, 10000) }) describe('onCompleteResolve', () => { beforeEach(() => { helper.updateConfiguration('coc.source.resolve.triggerCharacters', ['.']) }) it('should do resolve for complete item', async () => { let resolved = false disposables.push(sources.createSource({ name: 'resolve', doComplete: (_opt: CompleteOption) => Promise.resolve({ items: [{ word: 'foo' }] }), onCompleteResolve: item => { resolved = true item.info = 'detail' } })) await nvim.input('i.') await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], '.foo') expect(resolved).toBe(true) }, 10000) it('should cancel resolve request', async () => { let cancelled = false let called = false disposables.push(sources.createSource({ name: 'resolve', doComplete: (_opt: CompleteOption) => Promise.resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }), onCompleteResolve: async (item, _opt, token) => { called = true let res = await waitWithToken(200, token) cancelled = res item.info = 'info' } })) await nvim.input('i.') await helper.waitValue(() => { return called }, true) await nvim.call('coc#pum#_navigate', [1, 0]) await helper.waitValue(() => { return cancelled }, true) nvim.call('coc#pum#cancel', [], true) let floatWin = await helper.getFloat('pumdetail') expect(floatWin).toBeUndefined() }) it('should not throw error', async () => { let called = false disposables.push(sources.createSource({ name: 'resolve', doComplete: (_opt: CompleteOption) => Promise.resolve({ items: [{ word: 'foo' }] }), onCompleteResolve: async _item => { called = true throw new Error('custom error') } })) await nvim.input('i.') await helper.waitPopup() expect(called).toBe(true) let cmdline = await helper.getCmdline() expect(cmdline.includes('error')).toBe(false) }, 10000) it('should timeout on resolve', async () => { let called = false disposables.push(sources.createSource({ name: 'resolve', doComplete: (_opt: CompleteOption) => Promise.resolve({ items: [{ word: 'foo' }] }), onCompleteResolve: async item => { called = true await helper.wait(200) item.info = 'info' } })) await nvim.input('i.') await helper.waitPopup() await helper.waitValue(() => { return called }, true) let floatWin = await helper.getFloat('pumdetail') expect(floatWin).toBeUndefined() }, 10000) }) describe('trigger completion', () => { it('should trigger completion if triggerAfterInsertEnter is true', async () => { helper.updateConfiguration('suggest.triggerAfterInsertEnter', true) await nvim.command('edit t|setl buftype=nofile') await nvim.input('o') await helper.wait(10) expect(completion.isActivated).toBe(false) await helper.createDocument() await create(['fball', 'football'], false) await nvim.input('f') await nvim.input('') await nvim.input('A') await helper.waitPopup() expect(completion.isActivated).toBe(true) }, 10000) it('should trigger complete when trigger patterns match', async () => { let source: ISource = { priority: 99, enable: true, name: 'temp', triggerPatterns: [/EM/], sourceType: SourceType.Service, doComplete: (opt: CompleteOption) => { if (!opt.input.startsWith('EM')) return null return Promise.resolve({ items: [ { word: 'foo', filterText: 'EMfoo' }, { word: 'bar', filterText: 'EMbar' } ] }) }, } disposables.push(sources.addSource(source)) await nvim.input('i') await nvim.input('EM') await helper.waitPopup() let items = await helper.items() expect(items.length).toBe(2) }, 10000) it('should filter and sort on increment search', async () => { await create(['forceDocumentSync', 'format', 'fallback'], false) await nvim.input('f') await helper.waitPopup() await nvim.input('oa') await helper.waitPopup() let items = await helper.items() expect(items.findIndex(o => o.word == 'fallback')).toBe(-1) }, 10000) it('should not trigger on insert enter', async () => { await nvim.setLine('f') await create(['foo', 'bar'], false) await nvim.input('') await nvim.input('A') await helper.wait(1) let visible = await pumvisible() expect(visible).toBe(false) }) it('should filter on fast input', async () => { await create(['foo', 'bar'], false) await nvim.input('br') await helper.waitPopup() let items = await helper.items() let item = items.find(o => o.word == 'foo') expect(item).toBeFalsy() expect(items[0].word).toBe('bar') }, 10000) it('should filter completion when type none trigger character', async () => { let source: ISource = { name: 'test', priority: 10, enable: true, firstMatch: false, sourceType: SourceType.Native, triggerCharacters: [], doComplete: async () => { return Promise.resolve({ items: [{ word: 'if(' }] }) } } disposables.push(sources.addSource(source)) await nvim.setLine('') await nvim.input('iif') await helper.waitPopup() await nvim.input('(') await helper.wait(50) let res = await pumvisible() expect(res).toBe(true) }, 10000) it('should trigger on triggerCharacters', async () => { let source: ISource = { name: 'trigger', enable: true, triggerCharacters: ['.'], doComplete: async () => Promise.resolve({ items: [{ word: 'foo' }] }) } disposables.push(sources.addSource(source)) let source1: ISource = { name: 'trigger1', enable: true, triggerCharacters: ['.'], doComplete: async () => Promise.resolve({ items: [{ word: 'bar' }] }) } disposables.push(sources.addSource(source1)) await nvim.input('i.') await helper.waitPopup() let items = await helper.items() expect(items.length).toBe(2) }, 10000) it('should fix start column', async () => { let source: ISource = { name: 'test', priority: 10, enable: true, firstMatch: false, sourceType: SourceType.Native, triggerCharacters: [], doComplete: async () => { return Promise.resolve({ startcol: 0, items: [{ word: 'foo.bar' }] }) } } disposables.push(sources.addSource(source)) await nvim.setLine('foo.') await nvim.input('Ab') await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foo.bar') }, 10000) it('should should complete items without input', async () => { await workspace.document let source: ISource = { enable: true, name: 'trigger', priority: 10, sourceType: SourceType.Native, doComplete: async () => Promise.resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) } disposables.push(sources.addSource(source)) await nvim.command('inoremap coc#refresh()') await nvim.input('i') await helper.wait(30) await nvim.input('') await helper.waitPopup() let items = await helper.items() expect(items.length).toBeGreaterThan(1) }, 10000) it('should show float window', async () => { helper.updateConfiguration('suggest.floatConfig', { border: true, title: 'title' }) let source: ISource = { name: 'float', priority: 10, enable: true, sourceType: SourceType.Native, doComplete: () => Promise.resolve({ items: [{ word: 'foo', info: 'bar' }] }) } disposables.push(sources.addSource(source)) await nvim.input('if') await helper.waitPopup() let hasFloat = await nvim.call('coc#float#has_float') expect(hasFloat).toBe(1) let res = await helper.visible('foo', 'float') expect(res).toBe(true) }, 10000) it('should trigger on triggerPatterns', async () => { let source: ISource = { name: 'pattern', priority: 10, enable: true, sourceType: SourceType.Native, triggerPatterns: [/\w+\.$/], doComplete: async () => Promise.resolve({ items: [{ word: 'foo' }] }) } disposables.push(sources.addSource(source)) await nvim.input('ia.') await helper.waitPopup() let res = await helper.visible('foo', 'pattern') expect(res).toBe(true) }) it('should not trigger triggerOnly source', async () => { let fn = jest.fn() let source: ISource = { name: 'pattern', triggerOnly: true, priority: 10, enable: true, sourceType: SourceType.Native, triggerPatterns: [/^From:\s*/], doComplete: () => { fn() return { items: [{ word: 'foo' }] } } } disposables.push(sources.addSource(source)) await nvim.input('if') await helper.wait(20) expect(fn).toHaveBeenCalledTimes(0) }) it('should not trigger when cursor moved', async () => { let source: ISource = { name: 'trigger', priority: 10, enable: true, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async () => Promise.resolve({ items: [{ word: 'foo' }] }) } disposables.push(sources.addSource(source)) await nvim.setLine('.a') await nvim.input('A') await nvim.input('') await nvim.input('') await helper.wait(10) let visible = await pumvisible() expect(visible).toBe(false) }) it('should trigger when completion is not completed', async () => { let token: CancellationToken let promise = new Promise(resolve => { let source: ISource = { name: 'completion', priority: 10, enable: true, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async (opt, cancellationToken) => { if (opt.triggerCharacter != '.') { token = cancellationToken resolve(undefined) return new Promise>((resolve, reject) => { let timer = setTimeout(() => { resolve({ items: [{ word: 'foo' }] }) }, 200) if (cancellationToken.isCancellationRequested) { clearTimeout(timer) reject(new Error('Cancelled')) } }) } return Promise.resolve({ items: [{ word: 'bar' }] }) } } disposables.push(sources.addSource(source)) }) await nvim.input('if') await promise await nvim.input('.') await helper.waitPopup() await helper.visible('bar', 'completion') expect(token).toBeDefined() expect(token.isCancellationRequested).toBe(true) }, 10000) }) describe('completion results', () => { it('should limit results for low priority source', async () => { helper.updateConfiguration('suggest.lowPrioritySourceLimit', 2) await create(['filename', 'filepath', 'find', 'filter', 'findIndex'], true) let items = await helper.items() expect(items.length).toBe(2) }) it('should contains duplicated items when dup is 1', async () => { await create([{ word: 'foo', dup: 1 }, { word: 'foo', dup: 1 }], true) let items = await helper.items() expect(items.length).toBe(2) }) it('should limit result for high priority source', async () => { helper.updateConfiguration('suggest.highPrioritySourceLimit', 2) let source: ISource = { name: 'high', priority: 90, enable: true, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async () => Promise.resolve({ items: ['filename', 'filepath', 'filter', 'file'].map(key => ({ word: key })) }) } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.waitPopup() let items = await helper.items() expect(items.length).toBeGreaterThan(1) }, 10000) it('should truncate label of complete items', async () => { helper.updateConfiguration('suggest.formatItems', ['abbr']) helper.updateConfiguration('suggest.labelMaxLength', 10) let source: ISource = { name: 'high', priority: 90, enable: true, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async () => Promise.resolve({ items: ['a', 'b', 'c', 'd'].map(key => ({ word: key.repeat(20) })) }) } disposables.push(sources.addSource(source)) await nvim.input('i.') await helper.waitPopup() let winid = await nvim.call('coc#float#get_float_by_kind', ['pum']) as number let win = nvim.createWindow(winid) let buf = await win.buffer let lines = await buf.lines expect(lines[0].trim().length).toBe(10) }, 10000) it('should render labelDetails', async () => { helper.updateConfiguration('suggest.formatItems', ['abbr']) helper.updateConfiguration('suggest.labelMaxLength', 10) disposables.push(sources.createSource({ name: 'test', doComplete: (_opt: CompleteOption) => new Promise(resolve => { resolve({ items: [{ word: 'x', labelDetails: { detail: 'foo', description: 'bar' } }, { word: 'y'.repeat(8), labelDetails: { detail: 'a'.repeat(20), description: 'b'.repeat(20) } }] }) }) })) await nvim.input('i') triggerCompletion('test') await helper.waitPopup() let winid = await nvim.call('coc#float#get_float_by_kind', ['pum']) as number let win = nvim.createWindow(winid) let buf = await win.buffer let lines = await buf.lines expect(lines.length).toBe(2) expect(lines[0]).toMatch(/xfoo bar/) }, 10000) it('should delete previous items when complete items is null', async () => { let source1: ISource = { name: 'source1', priority: 90, enable: true, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async () => Promise.resolve({ items: [{ word: 'foo', dup: 1 }] }) } let source2: ISource = { name: 'source2', priority: 90, enable: true, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async (opt: CompleteOption) => { return opt.input == 'foo' ? null : { items: [{ word: 'foo', dup: 1 }], isIncomplete: true } } } disposables.push(sources.addSource(source1)) disposables.push(sources.addSource(source2)) await nvim.input('i') await nvim.input('.f') await helper.waitPopup() let items = await helper.items() expect(items.length).toEqual(2) await nvim.input('oo') await helper.waitValue(() => { return completion.activeItems?.length }, 1) items = await helper.items() expect(items.length).toEqual(1) expect(items[0].word).toBe('foo') }, 10000) it('should cancel completion on navigate', async () => { let source1: ISource = { name: 'source1', priority: 90, enable: true, sourceType: SourceType.Native, doComplete: async () => Promise.resolve({ items: [{ word: 'foo' }, { word: 'for' }] }) } let cancelled = false let source2: ISource = { name: 'source2', priority: 90, enable: true, sourceType: SourceType.Native, doComplete: async (_opt: CompleteOption, token) => { return new Promise(resolve => { let timer = setTimeout(() => { resolve({ items: [{ word: 'foobar' }] }) }, 500) token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) }) }) } } disposables.push(sources.addSource(source1)) disposables.push(sources.addSource(source2)) await nvim.input('i') await nvim.input('f') await helper.waitPopup() await nvim.input('') await helper.waitValue(() => { return cancelled }, true) }, 10000) }) describe('indent change', () => { it('should trigger completion after indent change', async () => { await helper.createDocument('t') let source: ISource = { name: 'source1', priority: 90, enable: true, sourceType: SourceType.Native, doComplete: async () => Promise.resolve({ items: [ { word: 'endif' }, { word: 'endfunction' } ] }) } disposables.push(sources.addSource(source)) await nvim.input('i') await nvim.input(' endi') await helper.waitPopup() await nvim.input('f') await helper.wait(10) await nvim.call('setline', ['.', 'endif']) await helper.waitValue(() => { return completion.option?.col }, 0) }, 10000) it('should not trigger completion after indent change with reTriggerAfterIndent disabled', async () => { helper.updateConfiguration('suggest.reTriggerAfterIndent', false) await helper.createDocument('t') let source: ISource = { name: 'source1', priority: 90, enable: true, sourceType: SourceType.Native, doComplete: async () => Promise.resolve({ items: [{ word: 'endif' }] }) } disposables.push(sources.addSource(source)) await nvim.input('i') await nvim.input(' endi') await helper.waitPopup() await nvim.input('f') await helper.wait(10) await nvim.call('setline', ['.', 'endif']) await helper.wait(10) let visible = await pumvisible() expect(visible).toBe(false) }, 10000) }) describe('Navigate list', () => { it('should navigate completion list', async () => { helper.updateConfiguration('suggest.noselect', true) await create(['foo', 'foot'], true) let items = completion.activeItems nvim.call('coc#pum#_navigate', [1, 1], true) await helper.waitValue(() => { return completion.selectedItem?.word == items[0].word }, true) nvim.call('coc#pum#_navigate', [0, 1], true) await helper.waitValue(() => { return completion.selectedItem }, undefined) completion.cancelAndClose() await events.fire('MenuPopupChanged', [{}]) expect(completion.isActivated).toBe(false) }) it('should not cancel when cursor moved to end of inserted word', async () => { helper.updateConfiguration('suggest.noselect', true) await create(['foo', 'foot'], true) let items = completion.activeItems let { option } = completion nvim.call('coc#pum#_navigate', [1, 1], true) let word = items[0].word await helper.waitValue(() => { return completion.selectedItem?.word == word }, true) completion.onCursorMovedI(option.bufnr, [option.linenr, option.col + byteLength(word) + 1], false) expect(completion.isActivated).toBe(true) }) }) describe('Character insert', () => { beforeAll(() => { let source: ISource = { name: 'insert', firstMatch: false, sourceType: SourceType.Native, triggerCharacters: ['.'], doComplete: async opt => { if (opt.word === 'f') return { items: [{ word: 'foo' }] } if (!opt.triggerCharacter) return { items: [] } let result = { items: [{ word: 'one' }, { word: 'two' }] } return Promise.resolve(result) } } sources.addSource(source) }) afterAll(() => { sources.removeSource('insert') }) it('should keep selected text after text change', async () => { let doc = await workspace.document await nvim.setLine('f') await nvim.input('A') await doc.synchronize() triggerCompletion('insert') await helper.waitPopup() let line = await nvim.line expect(line).toBe('f') await nvim.exec(` noa call setline('.', 'foobar') noa call cursor(1, 7) `) await helper.waitValue(async () => { return await pumvisible() }, false) }, 10000) }) }) ================================================ FILE: src/__tests__/completion/float.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import Floating from '../../completion/floating' import { getInsertWord, prefixWord } from '../../completion/pum' import sources from '../../completion/sources' import { CompleteResult, ExtendedCompleteItem, ISource, SourceType } from '../../completion/types' import { FloatConfig } from '../../types' import helper from '../helper' let nvim: Neovim let source: ISource beforeAll(async () => { await helper.setup() nvim = helper.nvim source = { name: 'float', priority: 10, enable: true, sourceType: SourceType.Native, doComplete: (): Promise> => Promise.resolve({ items: [{ word: 'foo', info: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' }, { word: 'foot', info: 'foot' }, { word: 'football', }] }) } sources.addSource(source) }) afterAll(async () => { sources.removeSource(source) await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('completion float', () => { it('should prefix word', () => { expect(prefixWord('foo', 0, '', 0)).toBe('foo') expect(prefixWord('foo', 1, '$foo', 0)).toBe('$foo') }) it('should get insert word', () => { expect(getInsertWord('word', [], 0)).toBe('word') expect(getInsertWord('word\nbar', [10], 2)).toBe('word') }) it('should cancel float window', async () => { await helper.edit() await nvim.setLine('f') await nvim.input('A') nvim.call('coc#start', { source: 'float' }, true) await helper.waitPopup() await helper.confirmCompletion(0) let hasFloat = await nvim.call('coc#float#has_float') expect(hasFloat).toBe(0) }) it('should adjust float window position', async () => { await helper.edit() await nvim.setLine(' '.repeat(70)) await nvim.input('Af') await helper.visible('foo', 'float') let floatWin = await helper.getFloat('pumdetail') let config = await floatWin.getConfig() expect(config.col + config.width).toBeLessThan(180) }) it('should redraw float window on item change', async () => { await helper.edit() await nvim.setLine(' '.repeat(70)) await nvim.input('Af') await helper.visible('foo', 'float') await nvim.call('coc#pum#select', [1, 1, 0]) let floatWin = await helper.getFloat('pumdetail') let buf = await floatWin.buffer let lines = await buf.lines expect(lines.length).toBeGreaterThan(0) expect(lines[0]).toMatch('foot') }) it('should hide float window when item info is empty', async () => { await helper.edit() await nvim.setLine(' '.repeat(70)) await nvim.input('Af') await helper.visible('foo', 'float') await nvim.call('coc#pum#select', [2, 1, 0]) let floatWin = await helper.getFloat('pumdetail') expect(floatWin).toBeUndefined() }) it('should hide float window after completion', async () => { await helper.edit() await nvim.setLine(' '.repeat(70)) await nvim.input('Af') await helper.visible('foo', 'float') await nvim.input('') await helper.wait(30) await nvim.input('') await helper.wait(30) let floatWin = await helper.getFloat('pumdetail') expect(floatWin).toBeUndefined() }) }) describe('float config', () => { beforeEach(async () => { await nvim.input('of') await helper.waitPopup() }) async function createFloat(config: Partial, docs = [{ filetype: 'txt', content: 'doc' }]): Promise { let floating = new Floating({ floatConfig: { border: true, ...config } }) floating.show(docs) return floating } async function getFloat(): Promise { let win = await helper.getFloat('pumdetail') return win ? win.id : -1 } async function getRelated(winid: number, kind: string): Promise { if (!winid || winid == -1) return -1 let win = nvim.createWindow(winid) let related = await win.getVar('related') as number[] if (!related || !related.length) return -1 for (let id of related) { let w = nvim.createWindow(id) let v = await w.getVar('kind') if (v == kind) { return id } } return -1 } it('should not shown with empty lines', async () => { await createFloat({}, [{ filetype: 'txt', content: '' }]) let floatWin = await helper.getFloat('pumdetail') expect(floatWin).toBeUndefined() }) it('should show window with border', async () => { await createFloat({ border: true, rounded: true, focusable: true }) let winid = await getFloat() expect(winid).toBeGreaterThan(0) let id = await getRelated(winid, 'border') expect(id).toBeGreaterThan(0) }) it('should change window highlights', async () => { await createFloat({ border: true, highlight: 'WarningMsg', borderhighlight: 'MoreMsg' }) let winid = await getFloat() expect(winid).toBeGreaterThan(0) let win = nvim.createWindow(winid) let res = await win.getOption('winhl') as string expect(res).toMatch('WarningMsg') let id = await getRelated(winid, 'border') expect(id).toBeGreaterThan(0) win = nvim.createWindow(id) res = await win.getOption('winhl') as string expect(res).toMatch('MoreMsg') }) it('should add shadow and winblend', async () => { await createFloat({ shadow: true, winblend: 30 }) let winid = await getFloat() expect(winid).toBeGreaterThan(0) }) }) ================================================ FILE: src/__tests__/completion/language.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable } from 'vscode-languageserver-protocol' import { CompletionItem, CompletionItemKind, CompletionList, InsertReplaceEdit, InsertTextFormat, InsertTextMode, Position, Range, TextEdit } from 'vscode-languageserver-types' import commandManager from '../../commands' import completion from '../../completion' import { fixIndent, fixTextEdit, getUltisnipOption } from '../../completion/source-language' import sources from '../../completion/sources' import { CompleteOption, InsertMode, ItemDefaults } from '../../completion/types' import events from '../../events' import languages from '../../languages' import { CompletionItemProvider } from '../../provider' import snippetManager from '../../snippets/manager' import { disposeAll } from '../../util' import window from '../../window' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) function createCompletionItem(word: string): CompletionItem { return { label: word, filterText: word } } describe('LanguageSource util', () => { it('should get ultisnip option', async () => { let item: CompletionItem = { label: 'label' } expect(getUltisnipOption(item)).toBeUndefined() item.data = {} expect(getUltisnipOption(item)).toBeUndefined() item.data.ultisnip = true expect(getUltisnipOption(item)).toBeDefined() item.data.ultisnip = {} expect(getUltisnipOption(item)).toBeDefined() }) it('should fix range from indent', async () => { let line = ' foo' let currline = 'foo' let range = Range.create(0, 2, 0, 5) expect(fixIndent(line, currline, range)).toBe(-2) expect(range).toEqual(Range.create(0, 0, 0, 3)) expect(fixIndent(currline, line, range)).toBe(2) expect(range).toEqual(Range.create(0, 2, 0, 5)) }) it('should fix textEdit', async () => { let edit = TextEdit.insert(Position.create(0, 1), '') expect((fixTextEdit(0, edit) as TextEdit).range.start.character).toBe(0) let insertReplaceEdit = InsertReplaceEdit.create('text', Range.create(0, 1, 0, 1), Range.create(0, 1, 0, 2)) fixTextEdit(0, insertReplaceEdit) expect(insertReplaceEdit.insert.start.character).toBe(0) expect(insertReplaceEdit.replace.start.character).toBe(0) fixTextEdit(0, insertReplaceEdit) expect(insertReplaceEdit.insert.start.character).toBe(0) expect(insertReplaceEdit.replace.start.character).toBe(0) }) it('should select recent item by prefix', async () => { helper.updateConfiguration('suggest.selection', 'recentlyUsedByPrefix', disposables) let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'fa' }, { label: 'fb' }, { label: 'foo', kind: CompletionItemKind.Class }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) completion.mru.clear() completion.mru.add('f', { kind: CompletionItemKind.Class, filterText: 'foo', source: sources.getSource('foo'), }) await nvim.setLine('f') await nvim.input('A') await nvim.call('coc#start', { source: 'foo' }) await helper.waitPopup() let info = await nvim.call('coc#pum#info') as any expect(info).toBeDefined() expect(info.word).toBe('foo') }) }) describe('language source', () => { describe('toggle()', () => { it('should toggle source', () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', detail: 'detail of foo' }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) let source = sources.getSource('foo') expect(source).toBeDefined() source.toggle() expect(source.enable).toBe(false) source.toggle() expect(source.enable).toBe(true) }) }) describe('shouldCommit()', () => { it('should check commit characters', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', detail: 'detail of foo' }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider, [], 3, ['.'])) let source = sources.getSource('foo') let item = createCompletionItem('foo') let res = source.shouldCommit(item, '.') expect(res).toBe(true) }) it('should not feedkeys when already inserted before', async () => { helper.updateConfiguration('suggest.acceptSuggestionOnCommitCharacter', true, disposables) let provider: CompletionItemProvider = { provideCompletionItems: async (_doc, pos): Promise => [{ label: 'foo', textEdit: TextEdit.replace(Range.create(pos.line, pos.character, pos.line, pos.character + 1), `foo($1)$0`), insertTextFormat: InsertTextFormat.Snippet, commitCharacters: ['('] }] } disposables.push(languages.registerCompletionItemProvider('language', 'l', ['*'], provider)) await nvim.command('startinsert') nvim.call('coc#start', [{ source: 'language' }], true) await helper.waitPopup() expect(completion.selectedItem).toBeDefined() await nvim.input('(') await helper.waitValue(() => completion.isActivated, false) }) it('should not feedkeys when have paried characters before', async () => { helper.updateConfiguration('suggest.acceptSuggestionOnCommitCharacter', true, disposables) let provider: CompletionItemProvider = { provideCompletionItems: async (_doc, pos): Promise => [{ label: 'foo', textEdit: TextEdit.replace(Range.create(pos.line, pos.character, pos.line, pos.character + 1), `foo()$0`), insertTextFormat: InsertTextFormat.Snippet, commitCharacters: ['('] }] } disposables.push(languages.registerCompletionItemProvider('language', 'l', ['*'], provider)) await nvim.call('cursor', [1, 1]) await nvim.command('startinsert') await nvim.setLine('') nvim.call('coc#start', [{ source: 'language' }], true) await helper.waitPopup() expect(completion.selectedItem).toBeDefined() await nvim.input('()') await helper.waitFor('getline', ['.'], 'foo()') }) }) describe('resolveCompletionItem()', () => { async function getDetailContent(): Promise { let winid = await nvim.call('coc#float#get_float_by_kind', ['pumdetail']) if (!winid) return let bufnr = await nvim.call('winbufnr', [winid]) as number let lines = await (nvim.createBuffer(bufnr)).lines return lines.join('\n') } it('should return null when canceled or no items returned', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider, [], 3, ['.'])) let source = sources.getSource('foo') let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let res = await source.doComplete(opt, CancellationToken.Cancelled) expect(res).toBeNull() res = await source.doComplete(opt, CancellationToken.None) expect(res).toBeNull() }) it('should add detail to preview when no resolve exists', async () => { await helper.createDocument('foo.vim') let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', detail: 'detail of foo' }, { label: 'bar', detail: 'bar()' }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', 'vim', provider)) let mode = await nvim.mode if (mode.mode !== 'i') { await nvim.input('i') } nvim.call('coc#start', [{ source: 'foo' }], true) await helper.waitPopup() await helper.waitValue(async () => { let content = await getDetailContent() return content && /foo/.test(content) }, true) await nvim.input('') await helper.waitValue(async () => { let content = await getDetailContent() return content && /bar/.test(content) }, true) }) it('should add documentation to preview when no resolve exists', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', labelDetails: {}, documentation: 'detail of foo' }, { label: 'bar', documentation: { kind: 'plaintext', value: 'bar' } }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) await nvim.input('i') await nvim.call('coc#start', { source: 'foo' }) await helper.waitPopup() await helper.wait(10) let content = await getDetailContent() expect(content).toMatch('foo') await nvim.input('') await helper.wait(30) content = await getDetailContent() expect(content).toMatch('bar') }) it('should resolve again when request cancelled', async () => { let count = 0 let cancelled = false let resolved = false let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'bar' }], resolveCompletionItem: (item, token) => { if (count === 0) { count++ return new Promise(resolve => { token.onCancellationRequested(() => { cancelled = true resolve(undefined) }) }) } resolved = true return item }, } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) await nvim.input('i') await nvim.call('coc#start', { source: 'foo' }) await helper.waitPopup() await helper.waitValue(() => { return cancelled }, true) nvim.call('coc#pum#close', ['confirm'], true) await helper.waitValue(() => { return resolved }, true) }) it('should resolve CompletionItem', async () => { let res: CompletionItem | Error | undefined let n = 0 let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'this', documentation: 'detail of this' }], resolveCompletionItem: item => { if (res instanceof Error) { throw res } else { n++ return res } } } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let source = sources.getSource('foo') await source.doComplete(opt, CancellationToken.None) let item = createCompletionItem('this') await source.onCompleteResolve(item, opt, CancellationToken.None) res = { label: 'this', textEdit: TextEdit.insert(Position.create(0, 0), 'this') } let p = n await source.onCompleteResolve(item, opt, CancellationToken.None) await source.onCompleteResolve(item, opt, CancellationToken.None) expect(n - p).toBe(1) res = new Error('resolve error') item = createCompletionItem('this') await expect(async () => { await source.onCompleteResolve(item, opt, CancellationToken.None) }).rejects.toThrow(Error) }) }) describe('command', () => { it('should invoke command', async () => { let id = 'test.command' let item: CompletionItem = { label: 'this', command: { command: id, title: id, arguments: [] } } let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [item] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) let opt = await nvim.call('coc#util#get_complete_option') as any opt.snippetsSupport = false opt.insertMode = InsertMode.Insert let source = sources.getSource('foo') await source.doComplete(opt, CancellationToken.None) await source.onCompleteDone(item, opt) let called = false commandManager.registerCommand(id, () => { called = true }) await source.onCompleteDone(item, opt) expect(called).toBe(true) }) }) describe('labelDetails', () => { it('should show labelDetails to documentation window', async () => { helper.updateConfiguration('suggest.labelMaxLength', 10, disposables) let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', labelDetails: { detail: 'foo'.repeat(5) } }, { label: 'bar', labelDetails: { description: 'bar'.repeat(5) } }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('i') await nvim.call('coc#start', { source: 'edits' }) let winid: number await helper.waitValue(async () => { winid = await nvim.call('coc#float#get_float_by_kind', ['pumdetail']) as number return winid > 0 }, true) let lines = await helper.getWinLines(winid) expect(lines[0]).toMatch('foo') await nvim.call('coc#pum#_navigate', [1, 1]) await helper.waitValue(async () => { lines = await helper.getWinLines(winid) return lines.join(' ').includes('bar') }, true) }) }) describe('additionalTextEdits', () => { it('should fix cursor position with plain text on additionalTextEdits', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', filterText: 'foo', additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'a\nbar')] }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('if') await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'barfoo') let col = await nvim.call('col', ['.']) expect(col).toBe(7) }) it('should fix cursor position with snippet on additionalTextEdits', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'if', insertTextFormat: InsertTextFormat.Snippet, textEdit: { range: Range.create(0, 0, 0, 1), newText: 'if($1)' }, additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')], preselect: true }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('ii') await helper.waitPopup() let res = await helper.items() let idx = res.findIndex(o => o.source?.name == 'edits') await helper.confirmCompletion(idx) await helper.waitFor('col', ['.'], 8) }) it('should fix cursor position with plain text snippet on additionalTextEdits', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'if', filterText: 'if', insertTextFormat: InsertTextFormat.Snippet, textEdit: { range: Range.create(0, 0, 0, 2), newText: 'do$0' }, additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')], preselect: true }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('iif') await helper.waitPopup() let items = await helper.items() let idx = items.findIndex(o => o.word == 'do' && o.source?.name == 'edits') await helper.confirmCompletion(idx) await helper.waitFor('getline', ['.'], 'bar do') await helper.waitFor('col', ['.'], 7) }) it('should fix cursor position with nested snippet on additionalTextEdits', async () => { let pos = await window.getCursorPosition() let range = Range.create(pos, pos) let res = await commandManager.executeCommand('editor.action.insertSnippet', TextEdit.replace(range, 'func($1)$0')) expect(res).toBe(true) let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'if', filterText: 'if', insertTextFormat: InsertTextFormat.Snippet, insertText: 'do$0', additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')], preselect: true }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('if') await helper.waitPopup() await helper.confirmCompletion(0) await events.race(['CompleteDone'], 200) let [, lnum, col] = await nvim.call('getcurpos') as [number, number, number] expect(lnum).toBe(1) expect(col).toBe(12) }) it('should fix cursor position and keep placeholder with snippet on additionalTextEdits', async () => { let text = 'foo0bar1' await nvim.setLine(text) let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'var', insertTextFormat: InsertTextFormat.Snippet, textEdit: { range: Range.create(0, text.length + 1, 0, text.length + 1), newText: '${1:foo} = foo0bar1' }, additionalTextEdits: [TextEdit.del(Range.create(0, 0, 0, text.length + 1))], preselect: true }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.'])) await nvim.input('A.') await helper.waitPopup() let res = await helper.items() let idx = res.findIndex(o => o.source?.name == 'edits') await helper.confirmCompletion(idx) await helper.waitFor('getline', ['.'], 'foo = foo0bar1') await helper.wait(50) expect(snippetManager.session).toBeDefined() let [, lnum, col] = await nvim.call('getcurpos') as [number, number, number] expect(lnum).toBe(1) expect(col).toBe(3) }) it('should not cancel current snippet session when additionalTextEdits inside snippet', async () => { await nvim.input('i') snippetManager.cancel() let pos = await window.getCursorPosition() let range = Range.create(pos, pos) await commandManager.executeCommand('editor.action.insertSnippet', TextEdit.replace(range, 'foo($1, $2)$0'), true) let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'bar', insertTextFormat: InsertTextFormat.Snippet, textEdit: { range: Range.create(0, 4, 0, 5), newText: 'bar($1)' }, additionalTextEdits: [TextEdit.del(Range.create(0, 0, 0, 3))] }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.'])) await nvim.input('b') await helper.waitPopup() let res = await helper.items() let idx = res.findIndex(o => o.source?.name == 'edits') await helper.confirmCompletion(idx) await helper.waitFor('getline', ['.'], '(bar(), )') await helper.waitFor('col', ['.'], 6) }) }) describe('filterText', () => { it('should fix input for snippet item', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', filterText: 'foo', insertText: '${1:foo}($2)', insertTextFormat: InsertTextFormat.Snippet, }] } disposables.push(languages.registerCompletionItemProvider('snippets-test', 'st', null, provider)) await nvim.input('if') await helper.waitPopup() await nvim.call('coc#pum#select', [0, 1, 0]) await helper.waitFor('getline', ['.'], 'foo()') }) it('should fix filterText of complete item', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'name', sortText: '11', textEdit: { range: Range.create(0, 1, 0, 2), newText: '?.name' } }] } disposables.push(languages.registerCompletionItemProvider('name', 'N', null, provider, ['.'])) await nvim.setLine('t') await nvim.input('A.') await helper.waitPopup() await helper.confirmCompletion(0) let line = await nvim.line expect(line).toBe('t?.name') }) }) describe('inComplete result', () => { it('should filter in complete request', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (doc, pos, token, context): Promise => { let option = (context as any).option if (context.triggerCharacter == '.') { return { isIncomplete: true, items: [ { label: 'foo' }, { label: 'bar' } ] } } if (option.input == 'f') { if (token.isCancellationRequested) return return { isIncomplete: true, items: [ { label: 'foo' } ] } } if (option.input == 'fo') { if (token.isCancellationRequested) return return { isIncomplete: false, items: [ { label: 'foo' } ] } } } } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.'])) await nvim.input('i.') await helper.waitPopup() await nvim.input('fo') await helper.waitValue(async () => { let items = await helper.items() return items.length }, 1) }) }) describe('itemDefaults', () => { async function start(item: CompletionItem, itemDefaults: ItemDefaults, triggerCharacters: string[] = []): Promise { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => { return { items: [item], itemDefaults, isIncomplete: false } } } disposables.push(languages.registerCompletionItemProvider('test', 't', null, provider, triggerCharacters)) await nvim.input('i') nvim.call('coc#start', [{ source: 'test' }], true) await helper.waitPopup() } it('should use range of editRange from itemDefaults', async () => { await nvim.call('setline', ['.', 'bar']) await start({ label: 'foo' }, { editRange: Range.create(0, 0, 0, 3) }) await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foo') }) it('should use commitCharacters from itemDefaults', async () => { let dispose = helper.updateConfiguration('suggest.acceptSuggestionOnCommitCharacter', true) await start({ label: 'foo' }, { commitCharacters: ['.'] }, ['.']) await nvim.input('.') // should trigger after commit await helper.waitFor('getline', ['.'], 'foo.') expect(events.completing).toBe(true) completion.cancelAndClose() dispose() }) it('should use replace range of editRange from itemDefaults', async () => { await nvim.call('setline', ['.', 'bar']) await start({ label: 'foo' }, { editRange: { insert: Range.create(0, 0, 0, 0), replace: Range.create(0, 0, 0, 3), } }) await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foo') }) it('should use insertTextFormat from itemDefaults', async () => { await nvim.call('cursor', [1, 1]) await start({ label: 'foo', insertText: 'foo($1)$0' }, { insertTextFormat: InsertTextFormat.Snippet, insertTextMode: InsertTextMode.asIs, data: {} }) await helper.confirmCompletion(0) await helper.waitValue(async () => { let line = await nvim.call('getline', ['.']) as string return line.startsWith('foo()') }, true) }) it('should use textEditText when exists with default range', async () => { await start({ label: 'foo', insertText: 'bar', textEditText: 'foofoo' }, { editRange: Range.create(0, 0, 0, 0) }) await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foofoo') }) }) describe('textEdit', () => { it('should not apply edits when line changed', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 0), 'foo($1)'), insertTextFormat: InsertTextFormat.Snippet }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) let source = sources.getSource('foo') expect(source).toBeDefined() let opt = await nvim.call('coc#util#get_complete_option') as any await source.doComplete(opt, CancellationToken.None) let item = createCompletionItem('foo') await nvim.call('append', [0, ['', '']]) await nvim.command('normal! G') await source.onCompleteDone(item, opt) let line = await nvim.line expect(line).toBe('') }) it('should use insert range', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', insertText: 'foo' }] } disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) let source = sources.getSource('foo') expect(source).toBeDefined() await nvim.setLine('foo') await nvim.input('I') let opt = await nvim.call('coc#util#get_complete_option') as any opt.insertMode = InsertMode.Insert await source.doComplete(opt, CancellationToken.None) let item = createCompletionItem('foo') await source.onCompleteDone(item, opt) let line = await nvim.line expect(line).toBe('foofoo') }) it('should fix replace range for paired characters', async () => { // LS may failed to replace paired character at the end await nvim.setLine('<>') await nvim.input('i') let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: '', filterText: '', // bad range textEdit: { range: Range.create(0, 0, 0, 0), newText: '' }, }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) nvim.call('coc#start', [{ source: 'edits' }], true) await helper.waitPopup() let idx = completion.activeItems.findIndex(o => o.word == '') expect(idx).toBeGreaterThan(-1) await helper.confirmCompletion(idx) await helper.waitFor('getline', ['.'], '') }) it('should not eat existing paired character on valid range', async () => { await nvim.setLine('fn bar() {}') await nvim.call('cursor', [1, 7]) await nvim.input('a') let provider: CompletionItemProvider = { provideCompletionItems: async (_, position): Promise => [{ label: '(x, y): (i32, i32)', filterText: '(x, y): (i32, i32)', textEdit: { range: Range.create(position.line, position.character, position.line, position.character), newText: '(x, y): (i32, i32)' }, }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) let source = sources.getSource('edits') expect(source).toBeDefined() let opt = await nvim.call('coc#util#get_complete_option') as any await source.doComplete(opt, CancellationToken.None) await source.onCompleteDone({ label: '(x, y): (i32, i32)', filterText: '(x, y): (i32, i32)', textEdit: { range: Range.create(0, 7, 0, 7), newText: '(x, y): (i32, i32)' }, }, opt) await helper.waitFor('getline', ['.'], 'fn bar((x, y): (i32, i32)) {}') }) it('should fix bad range', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foo', filterText: 'foo', textEdit: { range: Range.create(0, 0, 0, 0), newText: 'foo' }, }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('i') nvim.call('coc#start', [{ source: 'edits' }], true) await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foo') }) it('should applyEdits for empty word', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: '', filterText: '!', textEdit: { range: Range.create(0, 0, 0, 1), newText: 'foo' }, data: { word: '' } }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['!'])) await nvim.input('i!') await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'foo') }, 10000) it('should provide word when textEdit after startcol', async () => { // some LS would send textEdit after first character, // need fix the word from newText let provider: CompletionItemProvider = { provideCompletionItems: async (_, position): Promise => { if (position.line != 0) return null return [{ label: 'bar', textEdit: { range: Range.create(0, 1, 0, 1), newText: 'bar' } }, { label: 'bad', textEdit: { replace: Range.create(0, 1, 0, 1), insert: Range.create(0, 1, 0, 1), newText: 'bad' } }] } } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('ib') await helper.waitPopup() let items = completion.activeItems expect(items[0].word).toBe('bar') }, 10000) it('should adjust completion position by textEdit start position', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (_document, _position, _token, context): Promise => { if (!context.triggerCharacter) return return [{ label: 'foo', textEdit: { range: Range.create(0, 0, 0, 1), newText: '?foo' } }] } } disposables.push(languages.registerCompletionItemProvider('fix', 'f', null, provider, ['?'])) await nvim.input('i?') await helper.waitPopup() await helper.confirmCompletion(0) let line = await nvim.line expect(line).toBe('?foo') }, 10000) it('should fix range of removed text range', async () => { let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => { return [{ label: 'React', textEdit: { range: Range.create(0, 0, 0, 8), newText: 'import React$1 from "react"' }, insertTextFormat: InsertTextFormat.Snippet }] } } disposables.push(languages.registerCompletionItemProvider('fix', 'f', null, provider, ['?'])) await nvim.call('setline', ['.', 'import r;']) await nvim.call('cursor', [1, 8]) await nvim.input('a') await nvim.call('coc#start', { source: 'fix' }) await helper.waitPopup() await helper.confirmCompletion(0) await helper.waitFor('getline', ['.'], 'import React from "react";') }, 10000) }) }) ================================================ FILE: src/__tests__/completion/sources.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { CancellationToken, CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import { Around } from '../../completion/native/around' import { Buffer } from '../../completion/native/buffer' import { File, filterFiles, getDirectory, getFileItem, getItemsFromRoot, getLastPart, resolveEnvVariables } from '../../completion/native/file' import Source, { firstMatchFuzzy } from '../../completion/source' import VimSource, { checkInclude, getMethodName } from '../../completion/source-vim' import sources, { Sources, getSourceType, logError } from '../../completion/sources' import { CompleteOption, ExtendedCompleteItem, SourceConfig, SourceType } from '../../completion/types' import extensions from '../../extension' import { WordsSource } from '../../snippets/util' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let disposables: Disposable[] = [] const emptyFn = () => Promise.resolve(null) beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('KeywordsBuffer', () => { it('should parse keywords', async () => { let filepath = await createTmpFile(' ab\nab') let doc = await helper.createDocument(filepath) let b = sources.getKeywordsBuffer(doc.bufnr) let words = b.getWords() expect(words).toEqual(['ab']) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) words = b.getWords() expect(words).toEqual(['foo', 'bar', 'ab']) await doc.applyEdits([TextEdit.replace(Range.create(0, 0, 1, 3), 'def ')]) words = b.getWords() expect(words).toEqual(['def', 'ab']) }) it('should yield match words', async () => { let filepath = await createTmpFile(`_foo\nbar\n`) let doc = await helper.createDocument(filepath) let b = sources.getKeywordsBuffer(doc.bufnr) const getResults = (iterable: Iterable) => { let res: string[] = [] for (let word of iterable) { res.push(word) } return res } let iterable = b.matchWords(0) expect(getResults(iterable)).toEqual(['_foo', 'bar']) iterable = b.matchWords(2) expect(getResults(iterable)).toEqual(['_foo', 'bar']) }) }) describe('Source', () => { function createSource(opt: SourceConfig): Source { let s = new Source(opt) disposables.push(s) return s } function makeid(length) { let result = '' let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let charactersLength = characters.length for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)) } return result } it('should check trigger only source', async () => { expect(typeof Sources).toBe('function') logError('') let name = 'foo' let s = createSource({ name, triggerOnly: true, doComplete: emptyFn }) expect(s.triggerOnly).toBe(true) expect(s.triggerPatterns).toBeNull() s = createSource({ name, doComplete: emptyFn }) helper.updateConfiguration(`coc.source.${name}.triggerPatterns`, [null, 'foo']) expect(s.triggerOnly).toBe(true) }) it('should get source type', async () => { for (let t of [SourceType.Native, SourceType.Remote, SourceType.Service]) { expect(getSourceType(t)).toBeDefined() } }) it('should check complete', async () => { let name = 'foo' let s = createSource({ name, doComplete: emptyFn }) helper.updateConfiguration(`coc.source.${name}.disableSyntaxes`, ['comment']) await nvim.input('i') let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption opt.synname = 'Comment' expect(await s.checkComplete(opt)).toBe(false) let result = await s.doComplete(opt, CancellationToken.None) expect(result).toBeNull() opt.synname = 'String' expect(await s.checkComplete(opt)).toBe(true) opt.synname = '' expect(await s.checkComplete(opt)).toBe(true) s = createSource({ name, shouldComplete: () => { return Promise.resolve(false) }, doComplete: emptyFn }) expect(await s.checkComplete(opt)).toBe(false) }) it('should call optional functions', async () => { await nvim.input('i') let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let name = 'foo' let n = 0 let s = createSource({ name, doComplete: emptyFn, refresh: () => { n++ return Promise.resolve() }, onCompleteDone: () => { n++ return Promise.resolve() }, onCompleteResolve: () => { n++ return Promise.resolve() } }) // expect(s.optionalFns).toEqual([]) await s.refresh() await s.onCompleteDone({} as any, opt) await s.doComplete(opt, CancellationToken.None) await s.onCompleteResolve({} as any, opt, CancellationToken.None) expect(n).toBe(3) }) it('should get results', async () => { let name = 'foo' let s = createSource({ name, doComplete: emptyFn }) let words = [] for (let i = 0; i < 80000; i++) { words.push(makeid(10)) } let items: Set = new Set() let tokenSource = new CancellationTokenSource() let p = s.getResults([words], '_$c', '', items, tokenSource.token) tokenSource.cancel() let res = await p expect(res).toBe(true) let n = Date.now() p = s.getResults([words], '_$a', '', items, CancellationToken.None) let spy = jest.spyOn(Date, 'now').mockImplementation(() => { return n + 200 }) res = await p spy.mockRestore() words = [] for (let i = 0; i < 300; i++) { words.push('a' + makeid(10)) } items = new Set() res = await s.getResults([words], 'a', '', items, CancellationToken.None) expect(items.size).toBe(50) items = new Set() res = await s.getResults([['你好']], 'ni', '', items, CancellationToken.None) expect(items.size).toBe(1) }) }) describe('vim source', () => { function createSourceFile(name: string, content: string): string { let dir = path.join(os.tmpdir(), `coc/source`) fs.mkdirSync(dir, { recursive: true }) let filepath = path.join(dir, `${name}.vim`) fs.writeFileSync(filepath, content, 'utf8') return filepath } it('should not throw when pluginPath already used', async () => { await sources.createVimSources(process.cwd()) await sources.createVimSources(process.cwd()) }) it('should show error for bad source file', async () => { let filepath = createSourceFile('tmp', '') await sources.createVimSourceExtension(filepath) let line = await helper.getCmdline() expect(line).toMatch('Error') }) it('should register filetypes extension for vim source', async () => { let content = ` function! coc#source#foo#init() return {'filetypes': ['vim'], 'firstMatch': v:true} endfunction function! coc#source#foo#complete(opt, cb) abort call a:cb([]) endfunction ` let filepath = createSourceFile('foo', content) await sources.createVimSourceExtension(filepath) let ext = extensions.getExtension('coc-vim-source-foo') expect(ext).toBeDefined() await Promise.resolve(ext.deactivate()) }) it('should not run by check complete', async () => { let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let source = new VimSource({ name: 'vim', sourceType: SourceType.Remote, remoteFns: ['on_complete', 'on_enter'] }) helper.updateConfiguration('coc.source.vim.disableSyntaxes', ['comment']) helper.updateConfiguration('coc.source.vim.filetypes', ['vim']) opt.synname = 'VimComment' opt.filetype = 'vim' let res = await source.checkComplete(opt) expect(res).toBe(false) let result = await source.doComplete(opt, CancellationToken.None) expect(result).toBe(null) opt.synname = '' res = await source.checkComplete(opt) expect(res).toBe(true) result = await source.doComplete(opt, CancellationToken.Cancelled) expect(result).toBe(null) source.onEnter(999) let bufnr = await nvim.call('bufnr', ['%']) as number source.onEnter(bufnr) }) it('should register extension for vim source', async () => { let content = ` function! coc#source#foo#init() return {'firstMatch': v:true, 'isSnippet': v:true} endfunction function! coc#source#foo#on_enter(...) let g:coc_entered = 1 endfunction function! coc#source#foo#get_startcol(opt) if a:opt['col'] == 1 return 0 endif return a:opt['col'] endfunction function! coc#source#foo#complete(opt, cb) abort if a:opt['col'] == 0 call a:cb([{'word': '.f'}]) return endif call a:cb([]) endfunction ` let filepath = createSourceFile('foo', content) await sources.createVimSourceExtension(filepath) let source = sources.getSource('foo') expect(source).toBeDefined() let bufnr = await nvim.call('bufnr', ['%']) as number source.onEnter(bufnr) let val = await nvim.getVar('coc_entered') expect(val).toBe(1) await nvim.setLine('.') await nvim.input('A') let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let res = await source.doComplete(opt, CancellationToken.None) expect(res.startcol).toBe(0) expect(res.items).toEqual([{ word: '.f', isSnippet: true }]) opt.col = 2 res = await source.doComplete(opt, CancellationToken.None) expect(res).toBe(null) }) it('should not insert snippet when on_complete exists', async () => { let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let source = new VimSource({ name: 'vim', sourceType: SourceType.Remote, remoteFns: ['on_complete'] }) let item: ExtendedCompleteItem = { word: 'word', abbr: 'word', filterText: 'word', isSnippet: true, insertText: 'word($1)' } let spy = jest.spyOn(nvim, 'call').mockImplementation(() => { return undefined }) await source.refresh() await source.onCompleteDone(item, opt) spy.mockRestore() let line = await nvim.line expect(line).toBe('') }) it('should insert snippet', async () => { let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let source = new VimSource({ name: 'vim', sourceType: SourceType.Remote }) let item: ExtendedCompleteItem = { word: 'word', abbr: 'word', filterText: 'word', isSnippet: true, insertText: 'word($1)' } await source.onCompleteDone(item, opt) let line = await nvim.line expect(line).toBe('word()') }) }) describe('native sources', () => { it('should not complete when buffer not exists', async () => { let tokenSource = new CancellationTokenSource() let source = sources.getSource('around') let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption Object.assign(opt, { bufnr: -1, input: 'a' }) let res = await source.doComplete(opt, tokenSource.token) expect(res).toBeNull() }) it('should not complete when check failed', async () => { let tokenSource = new CancellationTokenSource() for (const name of ['around', 'buffer', 'file']) { let source = sources.getSource(name) let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let spy = jest.spyOn(source, 'checkComplete' as any).mockReturnValue(Promise.resolve(false)) let res = await source.doComplete(opt, tokenSource.token) spy.mockRestore() expect(res).toBeNull() } }) it('should not complete with empty input', async () => { for (const name of ['around', 'buffer']) { let tokenSource = new CancellationTokenSource() let source = sources.getSources({ source: name } as any)[0] let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let res = await source.doComplete(opt, tokenSource.token) expect(res).toBeNull() } }) it('should not complete when cancelled', async () => { let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption Object.assign(opt, { input: 'a' }) for (const name of ['around', 'buffer']) { let source = sources.getSource(name) let res = await source.doComplete(opt, CancellationToken.Cancelled) expect(res).toBeNull() } }) it('should resolveEnvVariables', () => { expect(resolveEnvVariables('%HOME%/data%x%', { HOME: '/home' })).toBe('/home/data%x%') expect(resolveEnvVariables('$HOME/${USER}/data', { HOME: '/home', USER: 'foo' })).toBe('/home/foo/data') expect(resolveEnvVariables('$PART/data', {})).toBe('$PART/data') }) it('should getDirectory', () => { expect(getDirectory('a/b', '/home')).toBe('/home/a') expect(getDirectory(__dirname, '/home')).toBe(path.dirname(__dirname)) }) it('should getItemsFromRoot', async () => { let res = await getItemsFromRoot('a/b', '/not_exists', true, []) expect(res).toEqual([]) }) it('should getLastPart', () => { expect(getLastPart('/a/b!/x/y')).toBe('/x/y') expect(getLastPart('/a/b /x/y')).toBe('/x/y') expect(getLastPart('xy /a/b\\ /x/y')).toBe('/a/b\\ /x/y') expect(getLastPart('/a/b/x/y!')).toBeNull() expect(getLastPart('x#/')).toBe('/') expect(getLastPart('x /')).toBe('/') expect(getLastPart('/')).toBe('/') }) it('should getFileItem', async () => { expect(await getFileItem(__dirname, '')).toBeDefined() expect(await getFileItem(__dirname, 'file_not_exists')).toBeNull() expect(await getFileItem(__dirname, path.basename(__filename))).toBeDefined() }) it('should filterFiles', () => { expect(filterFiles(['.a', '.b', null], false)).toEqual(['.a', '.b']) expect(filterFiles(['a.js', 'b.ts'], true, ['*.js'])).toEqual(['b.ts']) }) it('should getRoot', async () => { let file = new File(false) let filepath = __filename let cwd = process.cwd() let root = await file.getRoot('./a', '', '', cwd) expect(root).toBe(cwd) root = await file.getRoot('./a', '', filepath, cwd) expect(root).toBe(path.dirname(filepath)) root = await file.getRoot('/a/b/', '', filepath, cwd) expect(root).toBe('/a/b/') root = await file.getRoot('/a/b', '', filepath, cwd) expect(root).toBe('/a') root = await file.getRoot('', 'a/b/not_exists', filepath, cwd) expect(root).toBeUndefined() let dir = path.dirname(__dirname) let base = path.basename(__dirname) root = await file.getRoot('', base, __dirname, cwd) expect(root).toBe(dir) root = await file.getRoot('', base, '/a/b', dir) expect(root).toBe(dir) root = await file.getRoot('', '', '', dir) expect(root).toBe(dir) file.isWindows = true root = await file.getRoot('C:\\user', '', filepath, cwd) expect(root).toBe('C:\\') root = await file.getRoot('C:\\user\\', '', filepath, cwd) expect(root).toBe('C:\\user\\') let arr = file.triggerCharacters expect(arr.includes('\\')).toBe(true) }) it('should firstMatchFuzzy', async () => { expect(firstMatchFuzzy(97, true, '_a')).toBe(true) expect(firstMatchFuzzy(97, true, 'a')).toBe(true) expect(firstMatchFuzzy(97, true, 'A')).toBe(true) expect(firstMatchFuzzy(97, true, 'â')).toBe(true) expect(firstMatchFuzzy(226, false, 'â')).toBe(true) }) it('should works for around source', async () => { let doc = await workspace.document await nvim.setLine('foo ') await doc.synchronize() let { mode } = await nvim.mode expect(mode).toBe('n') await nvim.input('Af') await helper.waitPopup() let res = await helper.visible('foo', 'around') expect(res).toBe(true) await nvim.input('') }) it('should works for buffer source', async () => { await helper.createDocument() await nvim.command('set hidden') let doc = await helper.createDocument() await nvim.setLine('other') await nvim.command('bp') await doc.synchronize() let { mode } = await nvim.mode expect(mode).toBe('n') await nvim.input('io') let res = await helper.visible('other', 'buffer') expect(res).toBe(true) }) it('should trigger for inComplete complete', async () => { await nvim.setLine('foo') await nvim.input('A') let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption opt.triggerForInComplete = true let around = new Around(sources.keywords) let res = await around.doComplete(opt, CancellationToken.None) expect(res).toBeDefined() let buffer = new Buffer(sources.keywords) res = await buffer.doComplete(opt, CancellationToken.None) expect(res).toBeDefined() }) it('should fix col for file source', async () => { await nvim.command(`edit t|setl iskeyword+=/`) await nvim.setLine('./') await nvim.input('A') nvim.call('coc#start', { source: 'file' }, true) await helper.waitPopup() }) it('should trim ext for file source', async () => { let cwd = path.resolve(__dirname, '..') let file = path.join(cwd, 't.ts') await helper.edit(file) await nvim.setLine('./') await nvim.input('A') nvim.call('coc#start', { source: 'file' }, true) await helper.waitPopup() let items = helper.completion.activeItems let idx = items.findIndex(o => o.word.endsWith('.ts')) expect(idx).toBe(-1) }) it('should not complete when cancelled', async () => { await nvim.setLine('/foo') await nvim.input('A') let file = new File(false) let tokenSource = new CancellationTokenSource() let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let p = file.doComplete(opt, tokenSource.token) tokenSource.cancel() let res = await p expect(res).toBeNull() }) it('should complete with words source', async () => { let stats = sources.sourceStats() let find = stats.find(o => o.name === '$words') expect(find).toBeUndefined() expect(WordsSource).toBeDefined() let s = sources.getSource('$words') as WordsSource expect(s.name).toBe('$words') expect(s.shortcut).toBe('') expect(s.triggerOnly).toBe(true) s.words = ['foo', 'bar'] s.startcol = 1 await nvim.setLine('longwords') await nvim.input('A') nvim.call('coc#start', { source: '$words' }, true) await helper.waitPopup() let items = await helper.items() expect(items.map(o => o.word)).toEqual(['foo', 'bar']) }) it('should get method name', () => { expect(getMethodName('f', ['f', 'o'])).toBe('f') expect(getMethodName('foo', ['Foo', 'Bar'])).toBe('Foo') expect(() => { getMethodName('foo', ['Bar']) }).toThrow() expect(checkInclude('f', ['f', 'o'])).toBe(true) expect(checkInclude('b', ['f', 'o'])).toBe(false) expect(checkInclude('foo', ['Foo', 'Bar'])).toBe(true) }) }) ================================================ FILE: src/__tests__/completion/util.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CompletionItem, CompletionItemKind, CompletionItemTag, Disposable, InsertTextFormat, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import { sortItems } from '../../completion/complete' import { caseScore, matchScore, matchScoreWithPositions } from '../../completion/match' import sources from '../../completion/sources' import { CompleteOption, InsertMode, ISource, SortMethod } from '../../completion/types' import { checkIgnoreRegexps, Converter, ConvertOption, createKindMap, deltaCount, emptLabelDetails, getDetail, getDocumentations, getInput, getKindHighlight, getKindText, getPriority, getReplaceRange, getResumeInput, getWord, hasAction, highlightOffset, indentChanged, isWordCode, MruLoader, OptionForWord, Selection, shouldIndent, shouldStop, toCompleteDoneItem } from '../../completion/util' import { WordDistance } from '../../completion/wordDistance' import events, { InsertChange } from '../../events' import languages from '../../languages' import { Chars } from '../../model/chars' import { disposeAll } from '../../util' import { getCharCodes } from '../../util/fuzzy' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let disposables: Disposable[] = [] let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(() => { disposeAll(disposables) }) function getSource(): ISource { return sources.getSource('$words') } describe('util functions', () => { it('should toCompleteDoneItem', async () => { expect(toCompleteDoneItem(undefined, undefined)).toEqual({}) }) it('should getPriority', async () => { expect(getPriority(getSource(), 5)).toBe(5) }) it('should add documentation', () => { let docs = getDocumentations({ label: 'word', detail: 'detail' }, '') expect(docs).toEqual([{ filetype: 'txt', content: 'detail' }]) docs = getDocumentations({ label: 'word', documentation: { kind: 'plaintext', value: '' } }, '') expect(docs).toEqual([]) docs = getDocumentations({ label: 'word', detail: 'detail' }, '', true) expect(docs).toEqual([]) docs = getDocumentations({ label: 'word', detail: 'detail', documentation: { kind: 'markdown', value: 'markdown' } }, 'vim') expect(docs.length).toBe(2) docs = getDocumentations({ word: '' }, '', true) expect(docs).toEqual([]) docs = getDocumentations({ word: '', documentation: [{ content: 'content', filetype: 'vim' }] }, '', true) expect(docs).toEqual([{ content: 'content', filetype: 'vim' }]) docs = getDocumentations({ word: '', info: 'info' }, '', true) expect(docs).toEqual([{ content: 'info', filetype: 'txt' }]) }) it('should get detail doc', () => { let item: CompletionItem = { label: '', detail: 'detail', labelDetails: {} } expect(getDetail(item, '')).toEqual({ filetype: 'txt', content: 'detail' }) item = { label: '', detail: 'detail', labelDetails: { detail: 'detail', description: 'desc' } } expect(getDetail(item, '')).toEqual({ filetype: 'txt', content: 'detail desc' }) item = { label: '', detail: 'detail', labelDetails: { description: 'desc' } } expect(getDetail(item, '')).toEqual({ filetype: 'txt', content: ' desc' }) item = { label: '', detail: 'detail', labelDetails: { detail: 'detail' } } expect(getDetail(item, '')).toEqual({ filetype: 'txt', content: 'detail' }) item = { label: '', detail: 'detail()' } expect(getDetail(item, 'vim')).toEqual({ filetype: 'vim', content: 'detail()' }) }) it('should get deltaCount', () => { let base = { lnum: 1, col: 1, line: '', changedtick: 1, pre: '' } let insert: InsertChange = Object.assign({ insertChar: 's' }, base) expect(deltaCount(insert)).toBe(0) insert = Object.assign({ insertChar: 's', insertChars: ['s'] }, base) expect(deltaCount(insert)).toBe(0) insert = Object.assign({ insertChar: 's', insertChars: ['s', 's'] }, base, { pre: 's' }) expect(deltaCount(insert)).toBe(0) insert = Object.assign({ insertChar: '<', insertChars: ['<', '>'] }, base, { pre: '<', line: ''] }, base, { pre: '<', line: '<>' }) expect(deltaCount(insert)).toBe(1) }) it('should get caseScore', () => { expect(typeof caseScore(10, 10, 2)).toBe('number') }) it('should check action', async () => { expect(hasAction({ label: 'foo', additionalTextEdits: [] }, {})).toBe(false) expect(hasAction({ label: 'foo', insertTextFormat: InsertTextFormat.Snippet }, {})).toBe(true) }) it('should check indentChanged', () => { expect(indentChanged(undefined, [1, 1, ''], '')).toBe(false) expect(indentChanged({ word: 'foo' }, [1, 4, 'foo'], ' foo')).toBe(true) expect(indentChanged({ word: 'foo' }, [1, 4, 'bar'], ' foo')).toBe(false) }) it('should get highlight offset', () => { let n = highlightOffset(3, { abbr: 'abc', filterText: 'def' }) expect(n).toBe(-1) expect(highlightOffset(3, { abbr: 'abc', filterText: 'abc' })).toBe(3) expect(highlightOffset(3, { abbr: 'xy abc', filterText: 'abc' })).toBe(6) }) it('should getKindText', () => { expect(getKindText('t', new Map(), '')).toBe('t') let m = new Map() m.set(CompletionItemKind.Class, 'C') expect(getKindText(CompletionItemKind.Class, m, 'D')).toBe('C') expect(getKindText(CompletionItemKind.Class, new Map(), 'D')).toBe('D') }) it('should getKindHighlight', async () => { const testHi = (kind: number | string, res: string) => { expect(getKindHighlight(kind)).toBe(res) } testHi(CompletionItemKind.Class, 'CocSymbolClass') testHi(999, 'CocSymbolDefault') testHi('', 'CocSymbolDefault') }) it('should createKindMap', () => { let map = createKindMap({ constructor: 'C' }) expect(map.get(CompletionItemKind.Constructor)).toBe('C') map = createKindMap({ constructor: undefined }) expect(map.get(CompletionItemKind.Constructor)).toBe('') }) it('should checkIgnoreRegexps', () => { expect(checkIgnoreRegexps([], '')).toBe(false) expect(checkIgnoreRegexps(['^^*^^'], 'input')).toBe(false) expect(checkIgnoreRegexps(['^inp', '^ind'], 'input')).toBe(true) }) it('should getResumeInput', () => { let opt = { line: 'foo', colnr: 4, col: 1, position: { line: 0, character: 3 } } expect(getResumeInput(opt, '')).toBeNull() expect(getResumeInput(opt, 'f')).toBe('') expect(getResumeInput(opt, 'bar')).toBeNull() expect(getResumeInput(opt, 'foot')).toBe('oot') }) function createOption(bufnr: number, linenr: number, line: string, col: number): Pick { return { bufnr, linenr, line, col } } it('should check stop', () => { let opt = createOption(1, 1, 'a', 2) expect(shouldStop(1, { line: '', col: 2, lnum: 1, changedtick: 1, pre: '' }, opt)).toBe(true) expect(shouldStop(1, { line: '', col: 2, lnum: 1, changedtick: 1, pre: ' ' }, opt)).toBe(true) expect(shouldStop(1, { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'fo' }, opt)).toBe(true) expect(shouldStop(2, { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'foob' }, opt)).toBe(true) expect(shouldStop(1, { line: '', col: 2, lnum: 2, changedtick: 1, pre: 'foob' }, opt)).toBe(true) expect(shouldStop(1, { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'barb' }, opt)).toBe(true) }) it('should check indent', () => { let res = shouldIndent('0{,0},0),0],!^F,o,O,e,=endif,=enddef,=endfu,=endfor', 'endfor') expect(res).toBe(true) res = shouldIndent('', 'endfor') expect(res).toBe(false) res = shouldIndent('0{,0},0),0],!^F,o,O,e,=endif,=enddef,=endfu,=endfor', 'foo bar') expect(res).toBe(false) res = shouldIndent('=~endif,=enddef,=endfu,=endfor', 'Endif') expect(res).toBe(true) res = shouldIndent(' ', '') expect(res).toBe(false) res = shouldIndent('*=endif', 'endif') expect(res).toBe(false) res = shouldIndent('0=foo', ' foo') expect(res).toBe(true) }) it('should check isWordCode', () => { let chars = new Chars('@,_,#') expect(isWordCode(chars, 97, true)).toBe(true) expect(isWordCode(chars, 97, false)).toBe(true) expect(isWordCode(chars, 10, false)).toBe(false) expect(isWordCode(chars, 0xdc00, false)).toBe(false) expect(isWordCode(chars, 20320, true)).toBe(false) }) it('should consider none word character as input', () => { let chars = new Chars('@,_,#') let res = getInput(chars, 'a#b#', false) expect(res).toBe('a#b#') res = getInput(chars, '你b#', true) expect(res).toBe('b#') res = getInput(chars, '你b#', false) expect(res).toBe('b#') }) it('should check emptLabelDetails', () => { expect(emptLabelDetails(null)).toBe(true) expect(emptLabelDetails({})).toBe(true) expect(emptLabelDetails({ detail: '' })).toBe(true) expect(emptLabelDetails({ detail: 'detail' })).toBe(false) expect(emptLabelDetails({ description: 'detail' })).toBe(false) }) it('should get word from complete item', () => { let item: CompletionItem = { label: 'foo', textEdit: TextEdit.insert(Position.create(0, 0), '$foo\nbar') } let word = getWord(item, {}) expect(word).toBe('$foo') item = { label: 'foo', data: { word: '$foo' } } word = getWord(item, {}) expect(word).toBe('$foo') item = { label: 'foo', insertText: 'foo($1)' } word = getWord(item, { insertTextFormat: InsertTextFormat.Snippet }) expect(word).toBe('foo()') word = getWord(item, { insertTextFormat: InsertTextFormat.PlainText }) expect(word).toBe('foo($1)') item = { label: 'foo' } word = getWord(item, {}) expect(word).toBe('foo') item = { label: 'foo', insertText: 'foo' } word = getWord(item, { insertTextFormat: InsertTextFormat.Snippet }) expect(word).toBe('foo') item = { label: 'foo', insertText: 'foo($1)', kind: CompletionItemKind.Function } word = getWord(item, { insertTextFormat: InsertTextFormat.Snippet }) expect(word).toBe('foo') }) it('should get replace range', () => { let item: CompletionItem = { label: 'foo' } expect(getReplaceRange(item, undefined)).toBeUndefined() expect(getReplaceRange(item, undefined, 0)).toBeUndefined() expect(getReplaceRange(item, Range.create(0, 0, 0, 3), 0)).toEqual(Range.create(0, 0, 0, 3)) expect(getReplaceRange(item, { insert: Range.create(0, 0, 0, 0), replace: Range.create(0, 0, 0, 3), } , 0)).toEqual(Range.create(0, 0, 0, 3)) expect(getReplaceRange(item, { insert: Range.create(0, 0, 0, 0), replace: Range.create(0, 0, 0, 3), } , 0, InsertMode.Insert)).toEqual(Range.create(0, 0, 0, 0)) item.textEdit = TextEdit.replace(Range.create(0, 0, 0, 3), 'foo') expect(getReplaceRange(item, undefined, 0)).toEqual(Range.create(0, 0, 0, 3)) item.textEdit = { newText: 'foo', insert: Range.create(0, 0, 0, 0), replace: Range.create(0, 0, 0, 3), } expect(getReplaceRange(item, undefined, 0)).toEqual(Range.create(0, 0, 0, 3)) item.textEdit = { newText: 'foo', insert: Range.create(0, 1, 0, 0), replace: Range.create(0, 1, 0, 3), } expect(getReplaceRange(item, undefined, 0)).toEqual(Range.create(0, 0, 0, 3)) }) describe('Converter', () => { function create(inputStart: number, option: ConvertOption, opt: OptionForWord): Converter { return new Converter(inputStart, option, opt) } it('should get previous & after', () => { let opt = { line: '$foo', col: 1, position: Position.create(0, 1) } let option: ConvertOption = { insertMode: InsertMode.Replace, priority: 0, range: Range.create(0, 1, 0, 4), source: getSource(), } let c = create(1, option, opt) expect(c.getPrevious(0)).toBe('$') expect(c.getPrevious(0)).toBe('$') expect(c.getAfter(4)).toBe('foo') expect(c.getAfter(4)).toBe('foo') expect(c.getAfter(2)).toBe('f') }) it('should convert completion item', () => { let opt = { line: '', position: Position.create(0, 0) } let option: ConvertOption = { insertMode: InsertMode.Replace, range: Range.create(0, 0, 0, 0), priority: 0, source: getSource(), } let item: any = { label: 'f', insertText: 'f', score: 3, data: { optional: true, dup: 0 }, tags: [CompletionItemTag.Deprecated] } let c = create(0, option, opt) let res = c.convertToDurationItem(item) expect(res.abbr.endsWith('?')).toBe(true) expect(typeof res.sortText).toBe('string') expect(res.deprecated).toBe(true) expect(res.dup).toBe(false) }) it('should replace word after cursor', () => { let opt = { line: 'afoo', position: Position.create(0, 1) } let option: ConvertOption = { insertMode: InsertMode.Replace, range: Range.create(0, 1, 0, 1), priority: 0, source: getSource(), } let item: CompletionItem = { label: 'afoo', insertText: 'afoo', textEdit: TextEdit.replace(Range.create(0, 0, 0, 4), 'afoo'), } let c = create(1, option, opt) let res = c.convertToDurationItem(item) expect(res.character).toBe(0) expect(res.word).toBe('a') item.textEdit = TextEdit.replace(Range.create(0, 1, 0, 4), 'foo') item.labelDetails = { description: 'description' } res = c.convertToDurationItem(item) expect(res.character).toBe(1) expect(res.labelDetails).toBeDefined() }) it('should convert completion item', () => { let opt = { line: '@', position: Position.create(0, 1) } let option: ConvertOption = { range: Range.create(0, 0, 0, 1), insertMode: InsertMode.Replace, priority: 0, asciiMatch: false, source: getSource(), } let item: any = { word: '@foo', abbr: 'foo' } let c = create(1, option, opt) let res = c.convertToDurationItem(item) expect(res.filterText).toBe('@foo') expect(res.delta).toBe(1) }) }) describe('matchScore', () => { function score(word: string, input: string): number { return matchScore(word, getCharCodes(input)) } it('should match score for last letter', () => { expect(score('#!3', '3')).toBe(1) expect(score('bar', 'f')).toBe(0) }) it('should return 0 when not matched', () => { expect(score('and', '你')).toBe(0) expect(score('你and', '你的')).toBe(0) expect(score('fooBar', 'Bt')).toBe(0) expect(score('thisbar', 'tihc')).toBe(0) }) it('should match first letter', () => { expect(score('abc', '')).toBe(0) expect(score('abc', 'a')).toBe(5) expect(score('Abc', 'a')).toBe(2.5) expect(score('__abc', 'a')).toBe(2) expect(score('$Abc', 'a')).toBe(1) expect(score('$Abc', 'A')).toBe(2) expect(score('$Abc', '$A')).toBe(6) expect(score('$Abc', '$a')).toBe(5.5) expect(score('foo_bar', 'b')).toBe(2) expect(score('foo_Bar', 'b')).toBe(1) expect(score('_foo_Bar', 'b')).toBe(0.5) expect(score('_foo_Bar', 'f')).toBe(2) expect(score('bar', 'a')).toBe(1) expect(score('fooBar', 'B')).toBe(2) expect(score('fooBar', 'b')).toBe(1) expect(score('fobtoBar', 'bt')).toBe(2) }) it('should match follow letters', () => { expect(score('abc', 'ab')).toBe(6) expect(score('adB', 'ab')).toBe(5.75) expect(score('adb', 'ab')).toBe(5.1) expect(score('adCB', 'ab')).toBe(5.05) expect(score('a_b_c', 'ab')).toBe(6) expect(score('FooBar', 'fb')).toBe(3.25) expect(score('FBar', 'fb')).toBe(3) expect(score('FooBar', 'FB')).toBe(6) expect(score('FBar', 'FB')).toBe(6) expect(score('a__b', 'a__b')).toBe(8) expect(score('aBc', 'ab')).toBe(5.5) expect(score('a_B_c', 'ab')).toBe(5.75) expect(score('abc', 'abc')).toBe(7) expect(score('abc', 'aC')).toBe(0) expect(score('abc', 'ac')).toBe(5.1) expect(score('abC', 'ac')).toBe(5.75) expect(score('abC', 'aC')).toBe(6) }) it('should only allow search once', () => { expect(score('foobar', 'fbr')).toBe(5.2) expect(score('foobaRow', 'fbr')).toBe(5.85) expect(score('foobaRow', 'fbR')).toBe(6.1) expect(score('foobar', 'fa')).toBe(5.1) }) it('should have higher score for strict match', () => { expect(score('language-client-protocol', 'lct')).toBe(6.1) expect(score('language-client-types', 'lct')).toBe(7) }) it('should find highest score', () => { expect(score('ArrayRotateTail', 'art')).toBe(3.6) }) }) describe('matchScoreWithPositions', () => { function assertMatch(word: string, input: string, res: [number, ReadonlyArray] | undefined): void { let result = matchScoreWithPositions(word, getCharCodes(input)) if (!res) { expect(result).toBeUndefined() } else { expect(result).toEqual(res) } } it('should return undefined when not match found', () => { assertMatch('a', 'abc', undefined) assertMatch('a', '', undefined) assertMatch('ab', 'ac', undefined) }) it('should find matches by position fix', () => { assertMatch('this', 'tih', [5.6, [0, 1, 2]]) assertMatch('globalThis', 'tihs', [2.6, [6, 7, 8, 9]]) }) it('should find matched positions', () => { assertMatch('this', 'th', [6, [0, 1]]) assertMatch('foo_bar', 'fb', [6, [0, 4]]) assertMatch('assertMatch', 'am', [5.75, [0, 6]]) }) }) describe('wordDistance', () => { it('should empty when not enabled', async () => { let w = await WordDistance.create(false, {} as any, CancellationToken.None) expect(w.distance(Position.create(0, 0), {} as any)).toBe(0) }) it('should empty when selectRanges is empty', async () => { let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let w = await WordDistance.create(true, opt, CancellationToken.None) expect(w).toBe(WordDistance.None) }) it('should empty when timeout', async () => { disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(0, 0, 0, 1) }] } })) let spy = jest.spyOn(workspace, 'computeWordRanges').mockImplementation(() => { return new Promise(resolve => { setTimeout(() => { resolve(null) }, 50) }) }) let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let w = await WordDistance.create(true, opt, CancellationToken.None) spy.mockRestore() expect(w).toBe(WordDistance.None) }) it('should get distance', async () => { disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(0, 0, 1, 0), parent: { range: Range.create(0, 0, 3, 0) } }] } })) let filepath = await createTmpFile('foo bar\ndef', disposables) await helper.edit(filepath) let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption let w = await WordDistance.create(true, opt, CancellationToken.None) expect(w.distance(Position.create(1, 0), {} as any)).toBeGreaterThan(0) expect(w.distance(Position.create(0, 0), { word: '', kind: CompletionItemKind.Keyword } as any)).toBeGreaterThan(0) expect(w.distance(Position.create(0, 0), { word: 'not_exists' } as any)).toBeGreaterThan(0) expect(w.distance(Position.create(0, 0), { word: 'bar' } as any)).toBe(0) expect(w.distance(Position.create(0, 0), { word: 'def' } as any)).toBeGreaterThan(0) await nvim.call('cursor', [1, 2]) await events.fire('CursorMoved', [opt.bufnr, [1, 2]]) expect(w.distance(Position.create(0, 0), { word: 'bar' } as any)).toBe(0) }) it('should get same range', async () => { disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(0, 0, 1, 0), parent: { range: Range.create(0, 0, 3, 0) } }] } })) let spy = jest.spyOn(workspace, 'computeWordRanges').mockImplementation(() => { return Promise.resolve({ foo: [Range.create(0, 0, 0, 0)] }) }) let opt = await nvim.call('coc#util#get_complete_option') as any opt.word = '' let w = await WordDistance.create(true, opt, CancellationToken.None) spy.mockRestore() let res = w.distance(Position.create(0, 0), { word: 'foo' } as any) expect(res).toBe(0) }) }) describe('sortItems', () => { it('should sort items', () => { let emptyInput = false let defaultSortMethod: SortMethod = SortMethod.None let a: any = { abbr: 'a', character: 0, filterText: 'a', index: 0, source: '', word: 'a' } let b: any = { abbr: 'b', character: 0, filterText: 'b', index: 0, source: '', word: 'b' } const check = (ap: any, bp: any, res: number) => { let val = sortItems(emptyInput, defaultSortMethod, Object.assign(ap, a), Object.assign(bp, b)) expect(val).toBe(res) } check({ score: 1 }, { score: 2 }, 1) check({ priority: 1 }, { priority: 2 }, 1) check({ sortText: 'b' }, { sortText: 'a' }, 1) check({ sortText: 'a' }, { sortText: 'b' }, -1) check({ localBonus: 1 }, { localBonus: 2 }, 1) }) }) describe('MruLoader', () => { it('should add item without prefix', () => { let loader = new MruLoader() loader.add('foo', { kind: '', source: getSource(), filterText: 'foo' }) let item = { kind: CompletionItemKind.Class, source: getSource(), filterText: '$foo' } loader.add('foo', item) let score = loader.getScore('', item, Selection.RecentlyUsed) expect(score).toBeGreaterThan(-1) score = loader.getScore('a', item, Selection.RecentlyUsedByPrefix) expect(score).toBe(-1) score = loader.getScore('f', item, Selection.RecentlyUsed) expect(score).toBeGreaterThan(-1) }) }) }) ================================================ FILE: src/__tests__/configuration/configurationModel.test.ts ================================================ import * as assert from 'assert' import { join } from 'path' import { URI } from 'vscode-uri' import { Configuration } from '../../configuration/configuration' import { AllKeysConfigurationChangeEvent, ConfigurationChangeEvent } from '../../configuration/event' import { ConfigurationModel } from '../../configuration/model' import { ConfigurationModelParser } from '../../configuration/parser' import { mergeChanges } from '../../configuration/util' import { Registry } from '../../util/registry' import { IConfigurationRegistry, validateProperty, configurationDefaultsSchemaId, resourceLanguageSettingsSchemaId, allSettings, resourceSettings, Extensions, IConfigurationNode } from '../../configuration/registry' import { ConfigurationScope, ConfigurationTarget } from '../../configuration/types' import { Disposable } from 'vscode-languageserver-protocol' import { disposeAll } from '../../util' import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../util/jsonRegistry' describe('ConfigurationRegistry', () => { let disposables: Disposable[] = [] afterAll(() => { disposeAll(disposables) }) let configuration = Registry.as(Extensions.Configuration) function createNode(id: string): IConfigurationNode { return { id, properties: {} } } function length(obj: object): number { return Object.keys(obj).length } test('register and unregister configuration', () => { let node = createNode('test') node.properties['test.foo'] = { type: 'string', default: '', markdownDeprecationMessage: 'deprecated' } node.properties['test.bar'] = { type: 'string', scope: ConfigurationScope.APPLICATION, included: false } node.properties['test.resource'] = { type: 'boolean', scope: ConfigurationScope.RESOURCE, markdownDescription: '# Description', default: true } node.properties['test.language'] = { type: 'array', scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, default: [] } expect(typeof configurationDefaultsSchemaId).toBe('string') let called = 0 configuration.onDidSchemaChange(() => { called++ }, null, disposables) configuration.onDidUpdateConfiguration(() => { called++ }, null, disposables) configuration.registerConfigurations([node], false) expect(called).toBe(2) let other = createNode('other') other.scope = ConfigurationScope.RESOURCE other.properties['test.foo'] = { type: 'string' } configuration.registerConfiguration(other) configuration.registerConfigurations([other]) let keys = Object.keys(allSettings.properties) expect(keys.length).toBe(3) keys = Object.keys(resourceSettings.properties) expect(keys.length).toBe(2) expect(length(configuration.getConfigurationProperties())).toBe(3) expect(length(configuration.getExcludedConfigurationProperties())).toBe(1) let jsonRegistry = Registry.as(JSONExtensions.JSONContribution) let schemas = jsonRegistry.getSchemaContributions().schemas expect(schemas[resourceLanguageSettingsSchemaId]).toBeDefined() configuration.deregisterConfigurations([node]) keys = Object.keys(allSettings.properties) expect(keys.length).toBe(0) keys = Object.keys(resourceSettings.properties) expect(keys.length).toBe(0) let schema = schemas[resourceLanguageSettingsSchemaId] expect(schema.properties).toEqual({}) }) test('register with extension info', () => { let node = createNode('test') node.extensionInfo = { id: 'coc-test' } node.properties['test.foo'] = { type: 'string', default: '', description: 'foo' } node.properties['test.bar'] = { type: 'string', default: '', } configuration.registerConfiguration(node) expect(allSettings.properties['test.foo'].description).toBeDefined() expect(allSettings.properties['test.bar'].description).toBeDefined() configuration.deregisterConfigurations([node]) }) test('update configurations', () => { let called = 0 configuration.onDidSchemaChange(() => { called++ }, null, disposables) configuration.updateConfigurations({ add: [], remove: [] }) expect(called).toBe(1) }) test('validateProperty', () => { expect(validateProperty('', {} as any) != null).toBe(true) expect(validateProperty('[docker]') != null).toBe(true) expect(validateProperty('key')).toBeNull() }) }) function toConfigurationModel(content: any): ConfigurationModel { const parser = new ConfigurationModelParser('test') parser.parse(JSON.stringify(content)) return parser.configurationModel } describe('ConfigurationModelParser', () => { test('parser no error with empty text', async () => { const parser = new ConfigurationModelParser('test') parser.parse(' ') expect(parser.errors.length).toBe(0) }) test('parse invalid value', async () => { let parser = new ConfigurationModelParser('test') parser.parse(33 as any) expect(parser.errors.length).toBe(1) }) test('parse conflict properties', async () => { let parser = new ConfigurationModelParser('test') let called = false let s = jest.spyOn(console, 'error').mockImplementation(() => { called = true }) parser.parse(JSON.stringify({ x: 1, 'x.y': {} }, null, 2)) s.mockRestore() expect(called).toBe(true) }) test('parse configuration model with single override identifier', () => { const testObject = new ConfigurationModelParser('') testObject.parse(JSON.stringify({ '[x]': { a: 1 } })) expect(JSON.stringify(testObject.configurationModel.overrides)).toEqual(JSON.stringify([{ identifiers: ['x'], keys: ['a'], contents: { a: 1 } }])) }) test('parse configuration model with multiple override identifiers', () => { const testObject = new ConfigurationModelParser('') testObject.parse(JSON.stringify({ '[x][y]': { a: 1 } })) assert.deepStrictEqual(JSON.stringify(testObject.configurationModel.overrides), JSON.stringify([{ identifiers: ['x', 'y'], keys: ['a'], contents: { a: 1 } }])) }) test('parse configuration model with multiple duplicate override identifiers', () => { const testObject = new ConfigurationModelParser('') testObject.parse(JSON.stringify({ '[x][y][x][z]': { a: 1 } })) assert.deepStrictEqual(JSON.stringify(testObject.configurationModel.overrides), JSON.stringify([{ identifiers: ['x', 'y', 'z'], keys: ['a'], contents: { a: 1 } }])) }) test('parse with conflict properties', async () => { const testObject = new ConfigurationModelParser('') testObject.parse('{"x": 3, "x": 4}') }) }) describe('ConfigurationModel', () => { test('setValue for a key that has no sections and not defined', () => { const testObject = new ConfigurationModel({ a: { b: 1 } }, ['a.b']) testObject.setValue('f', 1) assert.deepStrictEqual(testObject.contents, { a: { b: 1 }, f: 1 }) assert.deepStrictEqual(testObject.keys, ['a.b', 'f']) let called = false let s = jest.spyOn(console, 'error').mockImplementation(() => { called = true }) testObject.setValue('a.b.c.d', { x: 3 }) s.mockRestore() }) test('setValue for a key that has no sections and defined', () => { const testObject = new ConfigurationModel({ a: { b: 1 }, f: 1 }, ['a.b', 'f']) testObject.setValue('f', 3) assert.deepStrictEqual(testObject.contents, { a: { b: 1 }, f: 3 }) assert.deepStrictEqual(testObject.keys, ['a.b', 'f']) }) test('setValue for a key that has sections and not defined', () => { const testObject = new ConfigurationModel({ a: { b: 1 }, f: 1 }, ['a.b', 'f']) testObject.setValue('b.c', 1) const expected: any = {} expected['a'] = { b: 1 } expected['f'] = 1 expected['b'] = Object.create(null) expected['b']['c'] = 1 expect(testObject.contents).toEqual(expected) // assert.deepStrictEqual(testObject.contents, expected) assert.deepStrictEqual(testObject.keys, ['a.b', 'f', 'b.c']) }) test('setValue for a key that has sections and defined', () => { const testObject = new ConfigurationModel({ a: { b: 1 }, b: { c: 1 }, f: 1 }, ['a.b', 'b.c', 'f']) testObject.setValue('b.c', 3) assert.deepStrictEqual(testObject.contents, { a: { b: 1 }, b: { c: 3 }, f: 1 }) assert.deepStrictEqual(testObject.keys, ['a.b', 'b.c', 'f']) }) test('setValue for a key that has sections and sub section not defined', () => { const testObject = new ConfigurationModel({ a: { b: 1 }, f: 1 }, ['a.b', 'f']) testObject.setValue('a.c', 1) assert.deepStrictEqual(testObject.contents, { a: { b: 1, c: 1 }, f: 1 }) assert.deepStrictEqual(testObject.keys, ['a.b', 'f', 'a.c']) }) test('setValue for a key that has sections and sub section defined', () => { const testObject = new ConfigurationModel({ a: { b: 1, c: 1 }, f: 1 }, ['a.b', 'a.c', 'f']) testObject.setValue('a.c', 3) assert.deepStrictEqual(testObject.contents, { a: { b: 1, c: 3 }, f: 1 }) assert.deepStrictEqual(testObject.keys, ['a.b', 'a.c', 'f']) }) test('setValue for a key that has sections and last section is added', () => { const testObject = new ConfigurationModel({ a: { b: {} }, f: 1 }, ['a.b', 'f']) testObject.setValue('a.b.c', 1) assert.deepStrictEqual(testObject.contents, { a: { b: { c: 1 } }, f: 1 }) assert.deepStrictEqual(testObject.keys, ['a.b.c', 'f']) }) test('removeValue: remove a non existing key', () => { const testObject = new ConfigurationModel({ a: { b: 2 } }, ['a.b']) testObject.removeValue('a.b.c') assert.deepStrictEqual(testObject.contents, { a: { b: 2 } }) assert.deepStrictEqual(testObject.keys, ['a.b']) }) test('removeValue: remove a single segmented key', () => { const testObject = new ConfigurationModel({ a: 1 }, ['a']) testObject.removeValue('a') assert.deepStrictEqual(testObject.contents, {}) assert.deepStrictEqual(testObject.keys, []) }) test('removeValue: remove a multi segmented key', () => { const testObject = new ConfigurationModel({ a: { b: 1 } }, ['a.b']) testObject.removeValue('a.b') assert.deepStrictEqual(testObject.contents, {}) assert.deepStrictEqual(testObject.keys, []) }) test('get overriding configuration model for an existing identifier', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: 1 }, [], [{ identifiers: ['c'], contents: { a: { d: 1 } }, keys: ['a'] }]) assert.deepStrictEqual(testObject.override('c').contents, { a: { b: 1, d: 1 }, f: 1 }) }) test('get overriding configuration model for an identifier that does not exist', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: 1 }, [], [{ identifiers: ['c'], contents: { a: { d: 1 } }, keys: ['a'] }]) assert.deepStrictEqual(testObject.override('xyz').contents, { a: { b: 1 }, f: 1 }) }) test('get overriding configuration when one of the keys does not exist in base', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: 1 }, [], [{ identifiers: ['c'], contents: { a: { d: 1 }, g: 1 }, keys: ['a', 'g'] }]) assert.deepStrictEqual(testObject.override('c').contents, { a: { b: 1, d: 1 }, f: 1, g: 1 }) }) test('get overriding configuration when one of the key in base is not of object type', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: 1 }, [], [{ identifiers: ['c'], contents: { a: { d: 1 }, f: { g: 1 } }, keys: ['a', 'f'] }]) assert.deepStrictEqual(testObject.override('c').contents, { a: { b: 1, d: 1 }, f: { g: 1 } }) }) test('get overriding configuration when one of the key in overriding contents is not of object type', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: { g: 1 } }, [], [{ identifiers: ['c'], contents: { a: { d: 1 }, f: 1 }, keys: ['a', 'f'] }]) assert.deepStrictEqual(testObject.override('c').contents, { a: { b: 1, d: 1 }, f: 1 }) }) test('get overriding configuration if the value of overriding identifier is not object', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: { g: 1 } }, [], [{ identifiers: ['c'], contents: 'abc', keys: [] }]) assert.deepStrictEqual(testObject.override('c').contents, { a: { b: 1 }, f: { g: 1 } }) }) test('get overriding configuration if the value of overriding identifier is an empty object', () => { const testObject = new ConfigurationModel( { a: { b: 1 }, f: { g: 1 } }, [], [{ identifiers: ['c'], contents: {}, keys: [] }]) assert.deepStrictEqual(testObject.override('c').contents, { a: { b: 1 }, f: { g: 1 } }) }) test('simple merge', () => { const base = new ConfigurationModel({ a: 1, b: 2 }, ['a', 'b']) const add = new ConfigurationModel({ a: 3, c: 4 }, ['a', 'c']) const result = base.merge(add) assert.deepStrictEqual(result.contents, { a: 3, b: 2, c: 4 }) assert.deepStrictEqual(result.keys, ['a', 'b', 'c']) }) test('recursive merge', () => { const base = new ConfigurationModel({ a: { b: 1 } }, ['a.b']) const add = new ConfigurationModel({ a: { b: 2 } }, ['a.b']) const result = base.merge(add) assert.deepStrictEqual(result.contents, { a: { b: 2 } }) assert.deepStrictEqual(result.getValue('a'), { b: 2 }) assert.deepStrictEqual(result.keys, ['a.b']) }) test('simple merge overrides', () => { const base = new ConfigurationModel({ a: { b: 1 } }, ['a.b'], [{ identifiers: ['c'], contents: { a: 2 }, keys: ['a'] }]) const add = new ConfigurationModel({ a: { b: 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { b: 2 }, keys: ['b'] }]) const result = base.merge(add) assert.deepStrictEqual(result.contents, { a: { b: 2 } }) assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { a: 2, b: 2 }, keys: ['a', 'b'] }]) assert.deepStrictEqual(result.override('c').contents, { a: 2, b: 2 }) assert.deepStrictEqual(result.keys, ['a.b']) }) test('recursive merge overrides', () => { const base = new ConfigurationModel({ a: { b: 1 }, f: 1 }, ['a.b', 'f'], [{ identifiers: ['c'], contents: { a: { d: 1 } }, keys: ['a'] }]) const add = new ConfigurationModel({ a: { b: 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { a: { e: 2 } }, keys: ['a'] }]) const result = base.merge(add) assert.deepStrictEqual(result.contents, { a: { b: 2 }, f: 1 }) assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { a: { d: 1, e: 2 } }, keys: ['a'] }]) assert.deepStrictEqual(result.override('c').contents, { a: { b: 2, d: 1, e: 2 }, f: 1 }) assert.deepStrictEqual(result.keys, ['a.b', 'f']) }) test('merge overrides when frozen', () => { const model1 = new ConfigurationModel({ a: { b: 1 }, f: 1 }, ['a.b', 'f'], [{ identifiers: ['c'], contents: { a: { d: 1 } }, keys: ['a'] }]).freeze() const model2 = new ConfigurationModel({ a: { b: 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { a: { e: 2 } }, keys: ['a'] }]).freeze() const result = new ConfigurationModel().merge(model1, model2) assert.deepStrictEqual(result.contents, { a: { b: 2 }, f: 1 }) assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { a: { d: 1, e: 2 } }, keys: ['a'] }]) assert.deepStrictEqual(result.override('c').contents, { a: { b: 2, d: 1, e: 2 }, f: 1 }) assert.deepStrictEqual(result.keys, ['a.b', 'f']) }) test('Test contents while getting an existing property', () => { let testObject = new ConfigurationModel({ a: 1 }) assert.deepStrictEqual(testObject.getValue('a'), 1) testObject = new ConfigurationModel({ a: { b: 1 } }) assert.deepStrictEqual(testObject.getValue('a'), { b: 1 }) }) test('Test contents are undefined for non existing properties', () => { const testObject = new ConfigurationModel({ awesome: true }) assert.deepStrictEqual(testObject.getValue('unknownproperty'), undefined) }) test('Test override gives all content merged with overrides', () => { const testObject = new ConfigurationModel({ a: 1, c: 1 }, [], [{ identifiers: ['b'], contents: { a: 2 }, keys: ['a'] }]) assert.deepStrictEqual(testObject.override('b').contents, { a: 2, c: 1 }) }) test('Test override when an override has multiple identifiers', () => { const testObject = new ConfigurationModel({ a: 1, c: 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { a: 2 }, keys: ['a'] }]) let actual = testObject.override('x') assert.deepStrictEqual(actual.contents, { a: 2, c: 1 }) assert.deepStrictEqual(actual.keys, ['a', 'c']) assert.deepStrictEqual(testObject.getKeysForOverrideIdentifier('x'), ['a']) actual = testObject.override('y') assert.deepStrictEqual(actual.contents, { a: 2, c: 1 }) assert.deepStrictEqual(actual.keys, ['a', 'c']) assert.deepStrictEqual(testObject.getKeysForOverrideIdentifier('y'), ['a']) }) test('Test override when an identifier is defined in multiple overrides', () => { const testObject = new ConfigurationModel({ a: 1, c: 1 }, ['a', 'c'], [{ identifiers: ['x'], contents: { a: 3, b: 1 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { a: 2 }, keys: ['a'] }]) const actual = testObject.override('x') assert.deepStrictEqual(actual.contents, { a: 3, c: 1, b: 1 }) assert.deepStrictEqual(actual.keys, ['a', 'c']) assert.deepStrictEqual(testObject.getKeysForOverrideIdentifier('x'), ['a', 'b']) }) test('Test merge when configuration models have multiple identifiers', () => { const testObject = new ConfigurationModel({ a: 1, c: 1 }, ['a', 'c'], [{ identifiers: ['y'], contents: { c: 1 }, keys: ['c'] }, { identifiers: ['x', 'y'], contents: { a: 2 }, keys: ['a'] }]) const target = new ConfigurationModel({ a: 2, b: 1 }, ['a', 'b'], [{ identifiers: ['x'], contents: { a: 3, b: 2 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { b: 3 }, keys: ['b'] }]) const actual = testObject.merge(target) assert.deepStrictEqual(actual.contents, { a: 2, c: 1, b: 1 }) assert.deepStrictEqual(actual.keys, ['a', 'c', 'b']) assert.deepStrictEqual(actual.overrides, [ { identifiers: ['y'], contents: { c: 1 }, keys: ['c'] }, { identifiers: ['x', 'y'], contents: { a: 2, b: 3 }, keys: ['a', 'b'] }, { identifiers: ['x'], contents: { a: 3, b: 2 }, keys: ['a', 'b'] }, ]) }) }) describe('CustomConfigurationModel', () => { test('simple merge using models', () => { const base = new ConfigurationModelParser('base') base.parse(JSON.stringify({ a: 1, b: 2 })) const add = new ConfigurationModelParser('add') add.parse(JSON.stringify({ a: 3, c: 4 })) const result = base.configurationModel.merge(add.configurationModel) assert.deepStrictEqual(result.contents, { a: 3, b: 2, c: 4 }) }) test('simple merge with an undefined contents', () => { let base = new ConfigurationModelParser('base') base.parse(JSON.stringify({ a: 1, b: 2 })) let add = new ConfigurationModelParser('add') let result = base.configurationModel.merge(add.configurationModel) assert.deepStrictEqual(result.contents, { a: 1, b: 2 }) base = new ConfigurationModelParser('base') add = new ConfigurationModelParser('add') add.parse(JSON.stringify({ a: 1, b: 2 })) result = base.configurationModel.merge(add.configurationModel) assert.deepStrictEqual(result.contents, { a: 1, b: 2 }) base = new ConfigurationModelParser('base') add = new ConfigurationModelParser('add') result = base.configurationModel.merge(add.configurationModel) assert.deepStrictEqual(result.contents, {}) }) test('Recursive merge using config models', () => { const base = new ConfigurationModelParser('base') base.parse(JSON.stringify({ a: { b: 1 } })) const add = new ConfigurationModelParser('add') add.parse(JSON.stringify({ a: { b: 2 } })) const result = base.configurationModel.merge(add.configurationModel) assert.deepStrictEqual(result.contents, { a: { b: 2 } }) }) test('Test contents while getting an existing property', () => { const testObject = new ConfigurationModelParser('test') testObject.parse(JSON.stringify({ a: 1 })) assert.deepStrictEqual(testObject.configurationModel.getValue('a'), 1) testObject.parse(JSON.stringify({ a: { b: 1 } })) assert.deepStrictEqual(testObject.configurationModel.getValue('a'), { b: 1 }) }) test('Test contents are undefined for non existing properties', () => { const testObject = new ConfigurationModelParser('test') testObject.parse(JSON.stringify({ awesome: true })) assert.deepStrictEqual(testObject.configurationModel.getValue('unknownproperty'), undefined) }) test('Test contents are undefined for undefined config', () => { const testObject = new ConfigurationModelParser('test') assert.deepStrictEqual(testObject.configurationModel.getValue('unknownproperty'), undefined) }) test('Test configWithOverrides gives all content merged with overrides', () => { const testObject = new ConfigurationModelParser('test') testObject.parse(JSON.stringify({ a: 1, c: 1, '[b]': { a: 2 } })) assert.deepStrictEqual(testObject.configurationModel.override('b').contents, { a: 2, c: 1, '[b]': { a: 2 } }) }) test('Test configWithOverrides gives empty contents', () => { const testObject = new ConfigurationModelParser('test') assert.deepStrictEqual(testObject.configurationModel.override('b').contents, {}) }) test('Test update with empty data', () => { const testObject = new ConfigurationModelParser('test') testObject.parse('') assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null)) assert.deepStrictEqual(testObject.configurationModel.keys, []) testObject.parse(null!) assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null)) assert.deepStrictEqual(testObject.configurationModel.keys, []) testObject.parse(undefined!) assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null)) assert.deepStrictEqual(testObject.configurationModel.keys, []) }) test('Test empty property is not ignored', () => { const testObject = new ConfigurationModelParser('test') testObject.parse(JSON.stringify({ '': 1 })) // deepStrictEqual seems to ignore empty properties, fall back // to comparing the output of JSON.stringify assert.strictEqual(JSON.stringify(testObject.configurationModel.contents), JSON.stringify({ '': 1 })) assert.deepStrictEqual(testObject.configurationModel.keys, ['']) }) }) describe('Configuration', () => { test('Test getConfigurationModel', () => { const parser = new ConfigurationModelParser('test') parser.parse(JSON.stringify({ a: 1 })) const con: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel()) expect(con.getConfigurationModel(ConfigurationTarget.Default)).toBeDefined() expect(con.getConfigurationModel(ConfigurationTarget.User)).toBeDefined() expect(con.getConfigurationModel(ConfigurationTarget.Workspace)).toBeDefined() expect(con.getConfigurationModel(ConfigurationTarget.WorkspaceFolder, 'folder')).toBeDefined() expect(con.getConfigurationModel(ConfigurationTarget.Memory)).toBeDefined() }) test('Test resolveFolder', async () => { const con: Configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) con.addFolderConfiguration('/a/b/c', new ConfigurationModel()) con.addFolderConfiguration('/a', new ConfigurationModel()) let res = con.resolveFolder('/a/b/c/d/e') expect(res).toBe('/a/b/c') }) test('Test inspect for overrideIdentifiers', () => { const defaultConfigurationModel = toConfigurationModel({ '[l1]': { a: 1 }, '[l2]': { b: 1 } }) const userConfigurationModel = toConfigurationModel({ '[l3]': { a: 2 } }) const workspaceConfigurationModel = toConfigurationModel({ '[l1]': { a: 3 }, '[l4]': { a: 3 } }) const workspaceFolderConfigurationModel = toConfigurationModel({ '[l3]': { a: 3 } }) const testObject: Configuration = new Configuration(defaultConfigurationModel, userConfigurationModel, workspaceConfigurationModel) testObject.updateFolderConfiguration('/foo', workspaceFolderConfigurationModel) const { overrideIdentifiers } = testObject.inspect('a', {}) assert.deepStrictEqual(overrideIdentifiers, ['l1', 'l3', 'l4']) let res = testObject.inspect('a', { overrideIdentifier: 'l1' }) expect(res.value).toBe(3) expect(res.default.override).toBe(1) expect(res.user).toBeUndefined() res = testObject.inspect('a', { overrideIdentifier: 'l3' }) expect(res.user).toEqual({ value: undefined, override: 2 }) res = testObject.inspect('a', { overrideIdentifier: 'l3', resource: '/foo/bar' }) expect(res.workspaceFolder).toEqual({ value: undefined, override: 3 }) testObject.updateValue('b', 3) res = testObject.inspect('b', {}) expect(res.memoryValue).toBe(3) res = testObject.inspect('b', { overrideIdentifier: 'l3' }) expect(res.memoryValue).toBe(3) const newModel = toConfigurationModel({ a: 4 }) testObject.compareAndUpdateFolderConfiguration('/foo', newModel) res = testObject.inspect('a', { resource: '/foo/bar' }) expect(res.workspaceFolderValue).toBe(4) testObject.compareAndUpdateFolderConfiguration('/foo', newModel) res = testObject.inspect('a', { resource: '/foo/bar' }) expect(res.workspaceFolderValue).toBe(4) }) test('Test update value', () => { const parser = new ConfigurationModelParser('test') parser.parse(JSON.stringify({ a: 1 })) const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel()) testObject.updateValue('a', 2) assert.strictEqual(testObject.getValue('a', {}), 2) }) test('Test update by resource', async () => { const parser = new ConfigurationModelParser('test') parser.parse(JSON.stringify({ a: 1 })) const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel()) testObject.updateValue('a', 2, { resource: 'file' }) testObject.updateValue('a', 3, { resource: 'file' }) assert.strictEqual(testObject.getValue('a', { resource: 'file' }), 3) testObject.updateValue('a', undefined, { resource: 'file' }) assert.strictEqual(testObject.getValue('a', { resource: 'file' }), 1) }) test('Test update value after inspect', () => { const parser = new ConfigurationModelParser('test') parser.parse(JSON.stringify({ a: 1 })) const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel()) testObject.inspect('a', {}) testObject.updateValue('a', 2) assert.strictEqual(testObject.getValue('a', {}), 2) }) test('Test compare and update default configuration', () => { const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) testObject.updateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on', })) const actual = testObject.compareAndUpdateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', '[markdown]': { 'editor.wordWrap': 'off' } }), ['editor.lineNumbers', '[markdown]']) assert.deepStrictEqual(actual, { keys: ['editor.lineNumbers', '[markdown]'], overrides: [['markdown', ['editor.wordWrap']]] }) let res = testObject.compareAndUpdateDefaultConfiguration(toConfigurationModel({ '[markdown]': { 'editor.lineNumbers': 'off', 'editor.wordWrap': 'on', 'editor.showbreak': 'off' } }), ['[markdown]']) expect(res.overrides).toEqual([ ['markdown', ['editor.lineNumbers', 'editor.showbreak', 'editor.wordWrap']] ]) res = testObject.compareAndUpdateDefaultConfiguration(toConfigurationModel({})) expect(res.overrides).toEqual([ [ 'markdown', [ 'editor.lineNumbers', 'editor.wordWrap', 'editor.showbreak', 'editor.lineNumbers', 'editor.wordWrap', 'editor.showbreak' ] ] ]) }) test('Test compare and update same configurationModel', async () => { const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) let res = testObject.compareAndUpdateUserConfiguration(testObject.user) expect(res.keys).toEqual([]) res = testObject.compareAndUpdateWorkspaceConfiguration(testObject.workspace) expect(res.keys).toEqual([]) res = testObject.compareAndUpdateDefaultConfiguration(testObject.defaults) expect(res.keys).toEqual([]) testObject.compareAndDeleteFolderConfiguration('/a/b') }) test('Test compare and update user configuration', () => { const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) testObject.updateUserConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, '[typescript]': { 'editor.wordWrap': 'off' } })) const actual = testObject.compareAndUpdateUserConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on', 'window.zoomLevel': 1, '[typescript]': { 'editor.wordWrap': 'on', 'editor.insertSpaces': false } })) assert.deepStrictEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] }) }) test('Test compare and update workspace configuration', () => { const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) testObject.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, '[typescript]': { 'editor.wordWrap': 'off' } })) const actual = testObject.compareAndUpdateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on', 'window.zoomLevel': 1, '[typescript]': { 'editor.wordWrap': 'on', 'editor.insertSpaces': false } })) assert.deepStrictEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] }) }) test('Test compare and update workspace folder configuration', () => { const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) testObject.updateFolderConfiguration(URI.file('file1').fsPath, toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, '[typescript]': { 'editor.wordWrap': 'off' } })) const actual = testObject.compareAndUpdateFolderConfiguration(URI.file('file1').fsPath, toConfigurationModel({ 'editor.lineNumbers': 'on', 'window.zoomLevel': 1, '[typescript]': { 'editor.wordWrap': 'on', 'editor.insertSpaces': false } })) assert.deepStrictEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] }) testObject.compareAndUpdateFolderConfiguration('/a/b', new ConfigurationModel()) expect(testObject.hasFolder('/a/b')).toBe(true) }) }) describe('ConfigurationChangeEvent', () => { test('changeEvent affecting keys with new configuration', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) const change = configuration.compareAndUpdateUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 1, 'workbench.editor.enablePreview': false, 'files.autoSave': 'off', })) const testObject = new ConfigurationChangeEvent(change, undefined, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview', 'files.autoSave']) assert.ok(testObject.affectsConfiguration('window.zoomLevel')) assert.ok(testObject.affectsConfiguration('window')) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview')) assert.ok(testObject.affectsConfiguration('workbench.editor')) assert.ok(testObject.affectsConfiguration('workbench')) assert.ok(testObject.affectsConfiguration('files')) assert.ok(testObject.affectsConfiguration('files.autoSave')) assert.ok(!testObject.affectsConfiguration('files.exclude')) assert.ok(!testObject.affectsConfiguration('[markdown]')) assert.ok(!testObject.affectsConfiguration('editor')) }) test('changeEvent affecting keys when configuration changed', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) configuration.updateUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 2, 'workbench.editor.enablePreview': true, 'files.autoSave': 'off', })) const data = configuration.toData() const change = configuration.compareAndUpdateUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 1, 'workbench.editor.enablePreview': false, 'files.autoSave': 'off', })) const testObject = new ConfigurationChangeEvent(change, data, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview']) assert.ok(testObject.affectsConfiguration('window.zoomLevel')) assert.ok(testObject.affectsConfiguration('window')) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview')) assert.ok(testObject.affectsConfiguration('workbench.editor')) assert.ok(testObject.affectsConfiguration('workbench')) assert.ok(!testObject.affectsConfiguration('files')) assert.ok(!testObject.affectsConfiguration('[markdown]')) assert.ok(!testObject.affectsConfiguration('editor')) }) test('changeEvent affecting overrides with new configuration', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) const change = configuration.compareAndUpdateUserConfiguration(toConfigurationModel({ 'files.autoSave': 'off', '[markdown]': { 'editor.wordWrap': 'off' }, '[typescript][jsonc]': { 'editor.lineNumbers': 'off' } })) const testObject = new ConfigurationChangeEvent(change, undefined, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['files.autoSave', '[markdown]', '[typescript][jsonc]', 'editor.wordWrap', 'editor.lineNumbers']) assert.ok(testObject.affectsConfiguration('files')) assert.ok(testObject.affectsConfiguration('files.autoSave')) assert.ok(!testObject.affectsConfiguration('files.exclude')) assert.ok(testObject.affectsConfiguration('[markdown]')) assert.ok(!testObject.affectsConfiguration('[markdown].editor')) assert.ok(!testObject.affectsConfiguration('[markdown].workbench')) assert.ok(testObject.affectsConfiguration('editor')) assert.ok(testObject.affectsConfiguration('editor.wordWrap')) assert.ok(testObject.affectsConfiguration('editor.lineNumbers')) assert.ok(testObject.affectsConfiguration('editor', { languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor', { languageId: 'jsonc' })) assert.ok(testObject.affectsConfiguration('editor', { languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.wordWrap', { languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { languageId: 'jsonc' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { languageId: 'typescript' })) assert.ok(!testObject.affectsConfiguration('editor.lineNumbers', { languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { languageId: 'jsonc' })) assert.ok(!testObject.affectsConfiguration('editor', { languageId: 'json' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize', { languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize')) assert.ok(!testObject.affectsConfiguration('window')) }) test('changeEvent affecting overrides when configuration changed', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) configuration.updateUserConfiguration(toConfigurationModel({ 'workbench.editor.enablePreview': true, '[markdown]': { 'editor.fontSize': 12, 'editor.wordWrap': 'off' }, '[css][scss]': { 'editor.lineNumbers': 'off', 'css.lint.emptyRules': 'error' }, 'files.autoSave': 'off', })) const data = configuration.toData() const change = configuration.compareAndUpdateUserConfiguration(toConfigurationModel({ 'files.autoSave': 'off', '[markdown]': { 'editor.fontSize': 13, 'editor.wordWrap': 'off' }, '[css][scss]': { 'editor.lineNumbers': 'relative', 'css.lint.emptyRules': 'error' }, 'window.zoomLevel': 1, })) const testObject = new ConfigurationChangeEvent(change, data, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', '[markdown]', '[css][scss]', 'workbench.editor.enablePreview', 'editor.fontSize', 'editor.lineNumbers']) assert.ok(!testObject.affectsConfiguration('files')) assert.ok(testObject.affectsConfiguration('[markdown]')) assert.ok(!testObject.affectsConfiguration('[markdown].editor')) assert.ok(!testObject.affectsConfiguration('[markdown].editor.fontSize')) assert.ok(!testObject.affectsConfiguration('[markdown].editor.wordWrap')) assert.ok(!testObject.affectsConfiguration('[markdown].workbench')) assert.ok(testObject.affectsConfiguration('[css][scss]')) assert.ok(testObject.affectsConfiguration('editor')) assert.ok(testObject.affectsConfiguration('editor', { languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor', { languageId: 'css' })) assert.ok(testObject.affectsConfiguration('editor', { languageId: 'scss' })) assert.ok(testObject.affectsConfiguration('editor.fontSize', { languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize', { languageId: 'css' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize', { languageId: 'scss' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { languageId: 'scss' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { languageId: 'css' })) assert.ok(!testObject.affectsConfiguration('editor.lineNumbers', { languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap')) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor', { languageId: 'json' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize', { languageId: 'json' })) assert.ok(testObject.affectsConfiguration('window')) assert.ok(testObject.affectsConfiguration('window.zoomLevel')) assert.ok(testObject.affectsConfiguration('window', { languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('window.zoomLevel', { languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('workbench')) assert.ok(testObject.affectsConfiguration('workbench.editor')) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview')) assert.ok(testObject.affectsConfiguration('workbench', { languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('workbench.editor', { languageId: 'markdown' })) }) test('changeEvent affecting workspace folders', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) configuration.updateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' })) configuration.updateFolderConfiguration(URI.file('folder1').fsPath, toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })) configuration.updateFolderConfiguration(URI.file('folder2').fsPath, toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true })) const data = configuration.toData() // const workspace = new Workspace('a', // [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), // new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), // new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]) const change = mergeChanges( configuration.compareAndUpdateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'native' })), configuration.compareAndUpdateFolderConfiguration(URI.file('folder1').fsPath, toConfigurationModel({ 'window.zoomLevel': 1, 'window.restoreFullscreen': false })), configuration.compareAndUpdateFolderConfiguration(URI.file('folder2').fsPath, toConfigurationModel({ 'workbench.editor.enablePreview': false, 'window.restoreWindows': false })) ) const testObject = new ConfigurationChangeEvent(change, data, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']) assert.ok(testObject.affectsConfiguration('window.zoomLevel')) assert.ok(testObject.affectsConfiguration('window.zoomLevel', URI.file('folder1'))) assert.ok(testObject.affectsConfiguration('window.zoomLevel', URI.file(join('folder1', 'file1')))) assert.ok(!testObject.affectsConfiguration('window.zoomLevel', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('window.zoomLevel', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('window.zoomLevel', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('window.zoomLevel', URI.file(join('folder3', 'file3')))) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen')) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', URI.file(join('folder1', 'file1')))) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', URI.file('folder1'))) assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', URI.file(join('folder3', 'file3')))) assert.ok(testObject.affectsConfiguration('window.restoreWindows')) assert.ok(testObject.affectsConfiguration('window.restoreWindows', URI.file('folder2'))) assert.ok(testObject.affectsConfiguration('window.restoreWindows', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('window.restoreWindows', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('window.restoreWindows', URI.file(join('folder1', 'file1')))) assert.ok(!testObject.affectsConfiguration('window.restoreWindows', URI.file(join('folder3', 'file3')))) assert.ok(testObject.affectsConfiguration('window.title')) assert.ok(testObject.affectsConfiguration('window.title', URI.file('folder1'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file(join('folder1', 'file1')))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('folder2'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file(join('folder2', 'file2')))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('folder3'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file(join('folder3', 'file3')))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file3'))) assert.ok(testObject.affectsConfiguration('window')) assert.ok(testObject.affectsConfiguration('window', URI.file('folder1'))) assert.ok(testObject.affectsConfiguration('window', URI.file(join('folder1', 'file1')))) assert.ok(testObject.affectsConfiguration('window', URI.file('folder2'))) assert.ok(testObject.affectsConfiguration('window', URI.file(join('folder2', 'file2')))) assert.ok(testObject.affectsConfiguration('window', URI.file('folder3'))) assert.ok(testObject.affectsConfiguration('window', URI.file(join('folder3', 'file3')))) assert.ok(testObject.affectsConfiguration('window', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('window', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window', URI.file('file3'))) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview')) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('folder2'))) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('folder1'))) assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file(join('folder1', 'file1')))) assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('folder3'))) assert.ok(testObject.affectsConfiguration('workbench.editor')) assert.ok(testObject.affectsConfiguration('workbench.editor', URI.file('folder2'))) assert.ok(testObject.affectsConfiguration('workbench.editor', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('workbench.editor', URI.file('folder1'))) assert.ok(!testObject.affectsConfiguration('workbench.editor', URI.file(join('folder1', 'file1')))) assert.ok(!testObject.affectsConfiguration('workbench.editor', URI.file('folder3'))) assert.ok(testObject.affectsConfiguration('workbench')) assert.ok(testObject.affectsConfiguration('workbench', URI.file('folder2'))) assert.ok(testObject.affectsConfiguration('workbench', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('workbench', URI.file('folder1'))) assert.ok(!testObject.affectsConfiguration('workbench', URI.file('folder3'))) assert.ok(!testObject.affectsConfiguration('files')) assert.ok(!testObject.affectsConfiguration('files', URI.file('folder1'))) assert.ok(!testObject.affectsConfiguration('files', URI.file(join('folder1', 'file1')))) assert.ok(!testObject.affectsConfiguration('files', URI.file('folder2'))) assert.ok(!testObject.affectsConfiguration('files', URI.file(join('folder2', 'file2')))) assert.ok(!testObject.affectsConfiguration('files', URI.file('folder3'))) assert.ok(!testObject.affectsConfiguration('files', URI.file(join('folder3', 'file3')))) }) test('changeEvent - all', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) configuration.updateFolderConfiguration(URI.file('file1').fsPath, toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })) const data = configuration.toData() const change = mergeChanges( configuration.compareAndUpdateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', '[markdown]': { 'editor.wordWrap': 'off' } }), ['editor.lineNumbers', '[markdown]']), configuration.compareAndUpdateUserConfiguration(toConfigurationModel({ '[json]': { 'editor.lineNumbers': 'relative' } })), configuration.compareAndUpdateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' })), configuration.compareAndDeleteFolderConfiguration(URI.file('file1').fsPath), configuration.compareAndUpdateFolderConfiguration(URI.file('file2').fsPath, toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true }))) const testObject = new ConfigurationChangeEvent(change, data, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows', 'editor.wordWrap']) assert.ok(testObject.affectsConfiguration('window.title')) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window')) assert.ok(testObject.affectsConfiguration('window', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('window', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.zoomLevel')) assert.ok(testObject.affectsConfiguration('window.zoomLevel', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('window.zoomLevel', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen')) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.restoreWindows')) assert.ok(testObject.affectsConfiguration('window.restoreWindows', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('window.restoreWindows', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview')) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('workbench.editor')) assert.ok(testObject.affectsConfiguration('workbench.editor', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('workbench.editor', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('workbench')) assert.ok(testObject.affectsConfiguration('workbench', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('workbench', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('files')) assert.ok(!testObject.affectsConfiguration('files', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('files', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('editor')) assert.ok(testObject.affectsConfiguration('editor', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('editor', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file1').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file1').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file1').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file2').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file2').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file2').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers')) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file1').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file1').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file1').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file2').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file2').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file2').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.wordWrap')) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file1').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file1').toString(), languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file1').toString(), languageId: 'typescript' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file2').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file2').toString(), languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file2').toString(), languageId: 'typescript' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize')) assert.ok(!testObject.affectsConfiguration('editor.fontSize', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('editor.fontSize', URI.file('file2'))) }) test('changeEvent affecting tasks and launches', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) const change = configuration.compareAndUpdateUserConfiguration(toConfigurationModel({ launch: { configuration: {} }, 'launch.version': 1, tasks: { version: 2 } })) const testObject = new ConfigurationChangeEvent(change, undefined, configuration) assert.deepStrictEqual(testObject.affectedKeys, ['launch', 'launch.version', 'tasks']) assert.ok(testObject.affectsConfiguration('launch')) assert.ok(testObject.affectsConfiguration('launch.version')) assert.ok(testObject.affectsConfiguration('tasks')) }) }) describe('AllKeysConfigurationChangeEvent', () => { test('changeEvent', () => { const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()) configuration.updateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', '[markdown]': { 'editor.wordWrap': 'off' } })) configuration.updateUserConfiguration(toConfigurationModel({ '[json]': { 'editor.lineNumbers': 'relative' } })) configuration.updateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' })) configuration.updateFolderConfiguration(URI.file('file1').fsPath, toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })) configuration.updateFolderConfiguration(URI.file('file2').fsPath, toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true })) const testObject = new AllKeysConfigurationChangeEvent(configuration, ConfigurationTarget.User) assert.deepStrictEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']) assert.ok(testObject.affectsConfiguration('window.title')) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('window.title', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window')) assert.ok(testObject.affectsConfiguration('window', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('window', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.zoomLevel')) assert.ok(testObject.affectsConfiguration('window.zoomLevel', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('window.zoomLevel', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen')) assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('window.restoreWindows')) assert.ok(testObject.affectsConfiguration('window.restoreWindows', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('window.restoreWindows', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview')) assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('workbench.editor')) assert.ok(testObject.affectsConfiguration('workbench.editor', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('workbench.editor', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('workbench')) assert.ok(testObject.affectsConfiguration('workbench', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('workbench', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('files')) assert.ok(!testObject.affectsConfiguration('files', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('files', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('editor')) assert.ok(testObject.affectsConfiguration('editor', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('editor', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file1').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file1').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file1').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file2').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file2').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor', { uri: URI.file('file2').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers')) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', URI.file('file1'))) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', URI.file('file2'))) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file1').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file1').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file1').toString(), languageId: 'typescript' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file2').toString(), languageId: 'json' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file2').toString(), languageId: 'markdown' })) assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { uri: URI.file('file2').toString(), languageId: 'typescript' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap')) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', URI.file('file2'))) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file1').toString(), languageId: 'json' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file1').toString(), languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file1').toString(), languageId: 'typescript' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file2').toString(), languageId: 'json' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file2').toString(), languageId: 'markdown' })) assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { uri: URI.file('file2').toString(), languageId: 'typescript' })) assert.ok(!testObject.affectsConfiguration('editor.fontSize')) assert.ok(!testObject.affectsConfiguration('editor.fontSize', URI.file('file1'))) assert.ok(!testObject.affectsConfiguration('editor.fontSize', URI.file('file2'))) }) }) ================================================ FILE: src/__tests__/configuration/configurations.test.ts ================================================ import fs from 'fs' import os from 'os' import path from 'path' import { v1 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import Configurations, { folderSettingsSchemaId, userSettingsSchemaId } from '../../configuration' import { ConfigurationModel } from '../../configuration/model' import ConfigurationProxy from '../../configuration/shape' import { FolderConfigutions } from '../../configuration/configuration' import { ConfigurationTarget, ConfigurationUpdateTarget } from '../../configuration/types' import { disposeAll, wait } from '../../util' import { remove } from '../../util/fs' import helper from '../helper' import { resourceLanguageSettingsSchemaId } from '../../configuration/registry' import { CONFIG_FILE_NAME } from '../../util/constants' const workspaceConfigFile = path.resolve(__dirname, `../sample/.vim/${CONFIG_FILE_NAME}`) function U(fsPath: string): string { return URI.file(fsPath).toString() } function createConfigurations(): Configurations { let userConfigFile = path.join(__dirname, './settings.json') return new Configurations(userConfigFile) } const disposables: Disposable[] = [] afterEach(() => { disposeAll(disposables) }) function generateTmpDir(): string { return path.join(os.tmpdir(), uuid()) } describe('FolderConfigutions', () => { it('should getConfigurationByResource', async () => { let c = new FolderConfigutions() expect(c.getConfigurationByResource('')).toBeUndefined() expect(c.getConfigurationByResource('file:///a')).toBeUndefined() let model = new ConfigurationModel() c.set(os.tmpdir(), model) let uri = URI.file(path.join(os.tmpdir(), 'a/foo.js')).toString() let res = c.getConfigurationByResource(uri) expect(res.model).toBe(model) }) }) describe('Configurations', () => { describe('markdownPreference', () => { it('should get markdown preferences', async () => { let configurations = createConfigurations() let preferences = configurations.markdownPreference expect(preferences).toEqual({ excludeImages: true, breaks: true }) }) }) describe('ConfigurationProxy', () => { it('should create file and parent folder when necessary', async () => { let folder = generateTmpDir() let uri = URI.file(path.join(folder, 'a/b/settings.json')) let proxy = new ConfigurationProxy({}, false) await proxy.modifyConfiguration(uri.fsPath, 'foo', true) let content = fs.readFileSync(uri.fsPath, 'utf8') expect(JSON.parse(content)).toEqual({ foo: true }) await proxy.modifyConfiguration(uri.fsPath, 'foo', false) content = fs.readFileSync(uri.fsPath, 'utf8') expect(JSON.parse(content)).toEqual({ foo: false }) await remove(folder) }) it('should get folder from resolver', async () => { let proxy = new ConfigurationProxy({ getWorkspaceFolder: (uri: string) => { let fsPath = URI.parse(uri).fsPath if (fsPath.startsWith(os.tmpdir())) { return { uri: URI.file(os.tmpdir()).toString(), name: 'tmp' } } if (fsPath.startsWith(os.homedir())) { return { uri: URI.file(os.homedir()).toString(), name: 'home' } } return undefined }, root: __dirname }) let uri = proxy.getWorkspaceFolder(URI.file(path.join(os.tmpdir(), 'foo')).toString()) expect(uri.fsPath.startsWith(os.tmpdir())).toBe(true) uri = proxy.getWorkspaceFolder(URI.file('abc').toString()) expect(uri).toBeUndefined() proxy = new ConfigurationProxy({}) uri = proxy.getWorkspaceFolder(URI.file(path.join(os.tmpdir(), 'foo')).toString()) expect(uri).toBeUndefined() }) }) describe('watchFile', () => { it('should watch user config file', async () => { let userConfigFile = path.join(os.tmpdir(), `settings-${uuid()}.json`) fs.writeFileSync(userConfigFile, '{"foo.bar": true}', { encoding: 'utf8' }) let conf = new Configurations(userConfigFile, undefined, false) disposables.push(conf) expect(conf.getDefaultResource()).toBe(undefined) await wait(50) fs.writeFileSync(userConfigFile, '{"foo.bar": false}', { encoding: 'utf8' }) await helper.waitValue(() => { let c = conf.getConfiguration('foo') return c.get('bar') }, false) fs.rmSync(userConfigFile, { recursive: true }) }) it('should watch folder config file', async () => { let dir = generateTmpDir() let configFile = path.join(dir, '.vim/coc-settings.json') fs.mkdirSync(path.dirname(configFile), { recursive: true }) fs.writeFileSync(configFile, '{"foo.bar": true}', { encoding: 'utf8' }) let conf = new Configurations('', { get root() { return dir }, modifyConfiguration: async () => {}, getWorkspaceFolder: () => { return URI.file(dir) } }, false) expect(conf.getDefaultResource()).toMatch('file:') disposables.push(conf) let uri = U(dir) let resolved = conf.locateFolderConfigution(uri) expect(resolved).toBeDefined() await wait(20) fs.writeFileSync(configFile, '{"foo.bar": false}', { encoding: 'utf8' }) await helper.waitValue(() => { let c = conf.getConfiguration('foo') return c.get('bar') }, false) }) }) describe('getJSONSchema()', () => { it('should getJSONSchema', () => { let userConfigFile = path.join(__dirname, '.vim/coc-settings.json') let conf = new Configurations(userConfigFile, undefined) expect(conf.getJSONSchema(userSettingsSchemaId)).toBeDefined() expect(conf.getJSONSchema(folderSettingsSchemaId)).toBeDefined() expect(conf.getJSONSchema(resourceLanguageSettingsSchemaId)).toBeDefined() expect(conf.getJSONSchema('vscode://not_exists')).toBeUndefined() }) }) describe('getDescription()', () => { it('should get description', () => { let userConfigFile = path.join(__dirname, '.vim/coc-settings.json') let conf = new Configurations(userConfigFile, undefined) expect(conf.getDescription('not_exists_key')).toBeUndefined() }) }) describe('addFolderFile()', () => { it('should not add invalid folder from cwd', async () => { let userConfigFile = path.join(__dirname, '.vim/coc-settings.json') let conf = new Configurations(userConfigFile, undefined, true, os.homedir()) let res = conf.folderToConfigfile(os.homedir()) expect(res).toBeUndefined() res = conf.folderToConfigfile(__dirname) expect(res).toBeUndefined() }) it('should add folder as workspace configuration', () => { let configurations = createConfigurations() disposables.push(configurations) let fired = false configurations.onDidChange(() => { fired = true }) configurations.addFolderFile(workspaceConfigFile) let resource = URI.file(path.resolve(workspaceConfigFile, '../../tmp')) let c = configurations.getConfiguration('coc.preferences', resource) let res = c.inspect('rootPath') expect(res.key).toBe('coc.preferences.rootPath') expect(res.workspaceFolderValue).toBe('./src') expect(c.get('rootPath')).toBe('./src') expect(fired).toBe(false) }) it('should not add invalid folders', async () => { let configurations = createConfigurations() expect(configurations.addFolderFile('ab')).toBe(false) }) it('should resolve folder configuration when possible', async () => { let configurations = createConfigurations() expect(configurations.locateFolderConfigution('test:///foo')).toBe(false) let fsPath = path.join(__dirname, `../sample/abc`) expect(configurations.locateFolderConfigution(URI.file(fsPath).toString())).toBe(true) fsPath = path.join(__dirname, `../sample/foo`) expect(configurations.locateFolderConfigution(URI.file(fsPath).toString())).toBe(true) }) }) describe('getConfiguration()', () => { it('should load default configurations', () => { let conf = new Configurations(undefined, { modifyConfiguration: async () => {} }) disposables.push(conf) expect(conf.configuration.defaults.contents.coc).toBeDefined() let c = conf.getConfiguration('languageserver') expect(c).toEqual({}) expect(c.has('not_exists')).toBe(false) }) it('should load configuration without folder configuration', async () => { let conf = new Configurations(undefined, { root: path.join(path.dirname(__dirname), 'sample'), modifyConfiguration: async () => {} }) disposables.push(conf) conf.addFolderFile(workspaceConfigFile) let c = conf.getConfiguration('coc.preferences') expect(c.rootPath).toBeDefined() c = conf.getConfiguration('coc.preferences', null) expect(c.rootPath).toBeUndefined() }) it('should inspect configuration', async () => { let conf = new Configurations() let c = conf.getConfiguration('suggest') let res = c.inspect('not_exists') expect(res.defaultValue).toBeUndefined() expect(res.globalValue).toBeUndefined() expect(res.workspaceValue).toBeUndefined() c = conf.getConfiguration() res = c.inspect('not_exists') expect(res.key).toBe('not_exists') }) it('should update memory config #1', () => { let conf = new Configurations() let fn = jest.fn() conf.onDidChange(e => { expect(e.affectsConfiguration('x')).toBe(true) fn() }) conf.updateMemoryConfig({ x: 1 }) let config = conf.configuration.memory expect(config.contents).toEqual({ x: 1 }) expect(fn).toHaveBeenCalled() expect(conf.configuration.workspace).toBeDefined() }) it('should update memory config #2', () => { let conf = new Configurations() conf.updateMemoryConfig({ x: 1 }) conf.updateMemoryConfig({ x: undefined }) let config = conf.configuration.user expect(config.contents).toEqual({}) }) it('should update memory config #3', () => { let conf = new Configurations() conf.updateMemoryConfig({ 'suggest.floatConfig': { border: true } }) conf.updateMemoryConfig({ 'x.y': { foo: 1 } }) let val = conf.getConfiguration() let res = val.get('suggest') as any expect(res.floatConfig).toEqual({ border: true }) res = val.get('x.y') as any expect(res).toEqual({ foo: 1 }) }) it('should handle errors', () => { let tmpFile = path.join(os.tmpdir(), uuid()) fs.writeFileSync(tmpFile, '{"x":', 'utf8') let conf = new Configurations(tmpFile) disposables.push(conf) let errors = conf.errors expect(errors.size).toBeGreaterThan(0) }) it('should get nested property', () => { let config = createConfigurations() disposables.push(config) let conf = config.getConfiguration('servers.c') let res = conf.get('trace.server', '') expect(res).toBe('verbose') }) it('should get user and workspace configuration', () => { let userConfigFile = path.join(__dirname, './settings.json') let configurations = new Configurations(userConfigFile) disposables.push(configurations) let data = configurations.configuration.toData() expect(data.user).toBeDefined() expect(data.workspace).toBeDefined() expect(data.defaults).toBeDefined() let value = configurations.configuration.getValue(undefined, {}) expect(value.foo).toBeDefined() expect(value.foo.bar).toBe(1) }) it('should update configuration', async () => { let configurations = createConfigurations() disposables.push(configurations) configurations.addFolderFile(workspaceConfigFile) let resource = URI.file(path.resolve(workspaceConfigFile, '../..')) let fn = jest.fn() configurations.onDidChange(e => { expect(e.affectsConfiguration('foo')).toBe(true) expect(e.affectsConfiguration('foo.bar')).toBe(true) expect(e.affectsConfiguration('foo.bar', 'file://tmp/foo.js')).toBe(false) fn() }) let config = configurations.getConfiguration('foo', resource) let o = config.get('bar') expect(o).toBe(1) await config.update('bar', 6) config = configurations.getConfiguration('foo', resource) expect(config.get('bar')).toBe(6) expect(fn).toHaveBeenCalledTimes(1) }) it('should remove configuration', async () => { let configurations = createConfigurations() disposables.push(configurations) configurations.addFolderFile(workspaceConfigFile) let resource = URI.file(path.resolve(workspaceConfigFile, '../..')) let fn = jest.fn() configurations.onDidChange(e => { expect(e.affectsConfiguration('foo')).toBe(true) expect(e.affectsConfiguration('foo.bar')).toBe(true) fn() }) let config = configurations.getConfiguration('foo', resource) let o = config.get('bar') expect(o).toBe(1) await config.update('bar', null, true) config = configurations.getConfiguration('foo', resource) expect(config.get('bar')).toBeUndefined() expect(fn).toHaveBeenCalledTimes(1) }) }) describe('changeConfiguration', () => { it('should change workspace configuration', async () => { let con = createConfigurations() let m = new ConfigurationModel({ x: { a: 1 } }, ['x.a']) con.changeConfiguration(ConfigurationTarget.Workspace, m, undefined) let res = con.getConfiguration('x') expect(res.a).toBe(1) }) it('should change default configuration', async () => { let m = new ConfigurationModel({ x: { a: 1 } }, ['x.a']) let con = createConfigurations() con.changeConfiguration(ConfigurationTarget.Default, m, undefined) let res = con.getConfiguration('x') expect(res.a).toBe(1) }) }) describe('update()', () => { it('should update workspace configuration', async () => { let target = ConfigurationUpdateTarget.Workspace let con = createConfigurations() let res = con.getConfiguration() await res.update('x', 3, target) let val = con.getConfiguration().get('x') expect(val).toBe(3) }) it('should show error when workspace folder not resolved', async () => { let called = false let s = jest.spyOn(console, 'error').mockImplementation(() => { called = true }) let con = new Configurations(undefined, { modifyConfiguration: async () => {}, getWorkspaceFolder: () => { return undefined } }) let conf = con.getConfiguration(undefined, 'file:///1') await conf.update('x', 3, ConfigurationUpdateTarget.WorkspaceFolder) s.mockRestore() expect(called).toBe(true) }) }) describe('getWorkspaceConfigUri()', () => { it('should not get config uri for undefined resource', async () => { let conf = createConfigurations() let res = conf.resolveWorkspaceFolderForResource() expect(res).toBeUndefined() }) it('should not get config folder same as home', async () => { let conf = new Configurations(undefined, { modifyConfiguration: async () => {}, getWorkspaceFolder: () => { return URI.file(os.homedir()) } }) let uri = U(__filename) let res = conf.resolveWorkspaceFolderForResource(uri) expect(res).toBeUndefined() }) it('should create config file for workspace folder', async () => { let folder = path.join(os.tmpdir(), `test-workspace-folder-${uuid()}`) let conf = new Configurations(undefined, { modifyConfiguration: async () => {}, getWorkspaceFolder: () => { return URI.file(folder) } }) let res = conf.resolveWorkspaceFolderForResource('file:///1') expect(res).toBe(folder) let configFile = path.join(folder, '.vim/coc-settings.json') expect(fs.existsSync(configFile)).toBe(true) res = conf.resolveWorkspaceFolderForResource('file:///1') expect(res).toBe(folder) }) }) }) ================================================ FILE: src/__tests__/configuration/settings.json ================================================ { "foo.bar": 1, "bar.foo": 2, "schema": { "https://example.com": "*.yaml" }, "servers": { "c": { "trace.server": "verbose" } } } ================================================ FILE: src/__tests__/configuration/util.test.ts ================================================ import * as assert from 'assert' import os from 'os' import { ParseError } from 'jsonc-parser' import { addToValueTree, toValuesTree, convertErrors, convertTarget, expand, expandObject, getConfigurationValue, getDefaultValue, mergeChanges, mergeConfigProperties, overrideIdentifiersFromKey, removeFromValueTree, scopeToOverrides, toJSONObject } from '../../configuration/util' import { ConfigurationTarget, ConfigurationUpdateTarget } from '../../configuration/types' describe('Configuration utils', () => { it('convert parse errors', () => { let content = 'foo' let errors: ParseError[] = [] errors.push({ error: 2, length: 10, offset: 1 }) let arr = convertErrors(content, errors) expect(arr.length).toBe(1) }) it('get default value', () => { expect(getDefaultValue(undefined)).toBeNull() expect(getDefaultValue('string')).toBe('') expect(getDefaultValue(['string'])).toBe('') expect(getDefaultValue('boolean')).toBe(false) expect(getDefaultValue('integer')).toBe(0) expect(getDefaultValue('number')).toBe(0) expect(getDefaultValue('array')).toEqual([]) expect(getDefaultValue('object')).toEqual({}) }) it('should expand', () => { expect(expand('${userHome}')).toBe(os.homedir()) expect(expand('${cwd}')).toBe(process.cwd()) expect(expand('${env:NODE_ENV}')).toBe('test') expect(expand('${env:NOT_EXISTS}')).toBe('${env:NOT_EXISTS}') expect(expandObject('${env:NODE_ENV}')).toBe('test') expect(expandObject(undefined)).toBe(undefined) let obj = { list: ['${env:NODE_ENV}', '', 1], val: '${env:NODE_ENV}' } let res = expandObject(obj) expect(res).toEqual({ list: ['test', '', 1], val: 'test' }) }) it('should convertTarget', () => { expect(convertTarget(ConfigurationUpdateTarget.Global)).toBe(ConfigurationTarget.User) expect(convertTarget(ConfigurationUpdateTarget.Workspace)).toBe(ConfigurationTarget.Workspace) expect(convertTarget(ConfigurationUpdateTarget.WorkspaceFolder)).toBe(ConfigurationTarget.WorkspaceFolder) }) it('should scopeToOverrides', () => { expect(scopeToOverrides(null)).toBeUndefined() }) it('should get overrideIdentifiersFromKey', () => { let res = overrideIdentifiersFromKey('[ ]') expect(res).toEqual([]) }) it('should merge properties', () => { let res = mergeConfigProperties({ foo: 'bar', "x.y.a": "x", "x.y.b": "y", "x.t": "z" }) expect(res).toEqual({ foo: 'bar', x: { y: { a: 'x', b: 'y' }, t: 'z' } }) }) it('should toValuesTree', () => { let res = toValuesTree({ 'x.y.z': '${env:NODE_ENV}', env: '${env:NODE_ENV}' }, () => {}, true) expect(res).toEqual({ x: { y: { z: 'test' } }, env: 'test' }) }) it('should addToValueTree conflict #1', () => { let fn = jest.fn() let obj = { x: 66 } addToValueTree(obj, 'x.y', '3', () => { fn() }, true) addToValueTree(obj, 'x.y', '3', () => {}) expect(fn).toHaveBeenCalled() }) it('should addToValueTree conflict #2', () => { let fn = jest.fn() addToValueTree(undefined, 'x', '3', () => { fn() }) addToValueTree(undefined, 'x', '3', () => {}) expect(fn).toHaveBeenCalled() }) it('should addToValueTree conflict #3', () => { let obj = { x: true } let fn = jest.fn() addToValueTree(obj, 'x.y', ['foo'], () => { fn() }) expect(fn).toHaveBeenCalled() }) it('removeFromValueTree: remove a non existing key', () => { let target = { a: { b: 2 } } removeFromValueTree(target, 'c') assert.deepStrictEqual(target, { a: { b: 2 } }) removeFromValueTree(target, 'c.d.e') assert.deepStrictEqual(target, { a: { b: 2 } }) }) it('removeFromValueTree: remove a multi segmented key from an object that has only sub sections of the key', () => { let target = { a: { b: 2 } } removeFromValueTree(target, 'a.b.c') assert.deepStrictEqual(target, { a: { b: 2 } }) }) it('removeFromValueTree: remove a single segmented key', () => { let target = { a: 1 } removeFromValueTree(target, 'a') assert.deepStrictEqual(target, {}) }) it('removeFromValueTree: remove a single segmented key when its value is undefined', () => { let target = { a: undefined } removeFromValueTree(target, 'a') assert.deepStrictEqual(target, {}) }) it('removeFromValueTree: remove a multi segmented key when its value is undefined', () => { let target = { a: { b: 1 } } removeFromValueTree(target, 'a.b') assert.deepStrictEqual(target, {}) }) it('removeFromValueTree: remove a multi segmented key when its value is array', () => { let target = { a: { b: [1] } } removeFromValueTree(target, 'a.b') assert.deepStrictEqual(target, {}) }) it('removeFromValueTree: remove a multi segmented key first segment value is array', () => { let target = { a: [1] } removeFromValueTree(target, 'a.0') assert.deepStrictEqual(target, { a: [1] }) }) it('removeFromValueTree: remove when key is the first segment', () => { let target = { a: { b: 1 } } removeFromValueTree(target, 'a') assert.deepStrictEqual(target, {}) }) it('removeFromValueTree: remove a multi segmented key when the first node has more values', () => { let target = { a: { b: { c: 1 }, d: 1 } } removeFromValueTree(target, 'a.b.c') assert.deepStrictEqual(target, { a: { d: 1 } }) }) it('removeFromValueTree: remove a multi segmented key when in between node has more values', () => { let target = { a: { b: { c: { d: 1 }, d: 1 } } } removeFromValueTree(target, 'a.b.c.d') assert.deepStrictEqual(target, { a: { b: { d: 1 } } }) }) it('removeFromValueTree: remove a multi segmented key when the last but one node has more values', () => { let target = { a: { b: { c: 1, d: 1 } } } removeFromValueTree(target, 'a.b.c') assert.deepStrictEqual(target, { a: { b: { d: 1 } } }) }) it('should convert errors', () => { let errors: ParseError[] = [] for (let i = 0; i < 17; i++) { errors.push({ error: i, offset: 0, length: 10 }) } // let res = convertErrors('file:///1', 'abc', errors) // expect(res.length).toBe(17) }) it('should get configuration value', () => { let root = { foo: { bar: 1, from: { to: 2 } }, bar: [1, 2] } let res = getConfigurationValue(root, 'foo.from.to', 1) expect(res).toBe(2) res = getConfigurationValue(root, 'foo.from', 1) expect(res).toEqual({ to: 2 }) }) it('should get json object', () => { let obj = [{ x: 1 }, { y: 2 }] expect(toJSONObject(obj)).toEqual(obj) }) }) describe('mergeChanges', () => { test('merge only keys', () => { const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] }) assert.deepStrictEqual(actual, { keys: ['a', 'b', 'c', 'd'], overrides: [] }) }) test('merge only keys with duplicates', () => { const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] }, { keys: ['a', 'd', 'e'], overrides: [] }) assert.deepStrictEqual(actual, { keys: ['a', 'b', 'c', 'd', 'e'], overrides: [] }) }) test('merge only overrides', () => { const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']]] }, { keys: [], overrides: [['b', ['3', '4']]] }) assert.deepStrictEqual(actual, { keys: [], overrides: [['a', ['1', '2']], ['b', ['3', '4']]] }) }) test('merge only overrides with duplicates', () => { const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: [], overrides: [['b', ['3', '4']]] }, { keys: [], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] }) assert.deepStrictEqual(actual, { keys: [], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] }) }) test('merge', () => { const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: ['b'], overrides: [['b', ['3', '4']]] }, { keys: ['c', 'a'], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] }) assert.deepStrictEqual(actual, { keys: ['b', 'c', 'a'], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] }) }) test('merge single change', () => { const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }) assert.deepStrictEqual(actual, { keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }) }) test('merge no changes', () => { const actual = mergeChanges() assert.deepStrictEqual(actual, { keys: [], overrides: [] }) }) }) ================================================ FILE: src/__tests__/core/autocmds.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, Emitter } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { AutocmdItem, createCommand, toAutocmdOption } from '../../core/autocmds' import events from '../../events' import { TextDocumentContentProvider } from '../../provider' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() disposeAll(disposables) }) describe('watchers', () => { it('should watch options', async () => { await events.fire('OptionSet', ['showmode', 0, 1]) let times = 0 let fn = () => { times++ } let disposable = workspace.watchOption('showmode', fn) disposables.push(workspace.watchOption('showmode', jest.fn())) nvim.command('set showmode', true) expect(workspace.watchers.options.length).toBeGreaterThan(0) await helper.waitValue(() => times, 1) disposable.dispose() nvim.command('set noshowmode', true) await helper.wait(20) expect(times).toBe(1) }) it('should watch global', async () => { await events.fire('GlobalChange', ['x', 0, 1]) let times = 0 let fn = () => { times++ } let disposable = workspace.watchGlobal('x', fn) workspace.watchGlobal('x', undefined, disposables) workspace.watchGlobal('x', undefined, disposables) await nvim.command('let g:x = 1') await helper.waitValue(() => times, 1) disposable.dispose() await nvim.command('let g:x = 2') await helper.wait(20) expect(times).toBe(1) }) it('should show error on watch callback error', async () => { let called = false let fn = () => { called = true throw new Error('error') } workspace.watchOption('showmode', fn, disposables) nvim.command('set showmode', true) await helper.waitValue(() => called, true) let line = await helper.getCmdline() expect(line).toMatch('Error on OptionSet') called = false workspace.watchGlobal('y', fn, disposables) await nvim.command('let g:y = 2') await helper.waitValue(() => called, true) line = await helper.getCmdline() expect(line).toMatch('Error on GlobalChange') }) }) describe('contentProvider', () => { it('should not throw for scheme not registered', async () => { await workspace.contentProvider.onBufReadCmd('not_exists', '') }) it('should register document content provider', async () => { let provider: TextDocumentContentProvider = { provideTextDocumentContent: (_uri, _token): string => 'sample text' } workspace.registerTextDocumentContentProvider('test', provider) await nvim.command('edit test://1') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual(['sample text']) }) it('should react on change event of document content provider', async () => { let text = 'foo' let emitter = new Emitter() let event = emitter.event let provider: TextDocumentContentProvider = { onDidChange: event, provideTextDocumentContent: (_uri, _token): string => text } workspace.registerTextDocumentContentProvider('jdk', provider) await nvim.command('edit jdk://1') let doc = await workspace.document text = 'bar' emitter.fire(URI.parse('jdk://1')) await helper.waitFor('getline', ['.'], 'bar') await nvim.command('bwipeout!') await helper.waitValue(() => doc.attached, false) emitter.fire(URI.parse('jdk://1')) }) }) describe('setupDynamicAutocmd()', () => { afterEach(() => { nvim.command(`autocmd! coc_dynamic_autocmd`, true) }) it('should create command', () => { let res = createCommand(1, 'BufEnter', { callback: () => {}, event: ['User Jump'], once: true, nested: true, arglist: ['3', '4'], request: true, }) expect(res).toBe(`autocmd coc_dynamic_autocmd BufEnter ++once ++nested call coc#rpc#request('doAutocmd', [1, 3, 4])`) }) it('should convert to autocmd option ', () => { let item = new AutocmdItem(1, { stack: '', buffer: 1, pattern: '*.js', once: true, nested: true, arglist: ['2', '3'], event: 'BufEnter', callback: () => {} }) let res = toAutocmdOption(item) expect(res).toEqual({ group: "coc_dynamic_autocmd", buffer: 1, pattern: "*.js", once: true, nested: true, command: "call coc#rpc#notify('doAutocmd', [1, 2, 3])" }) }) it('should setup autocmd', async () => { await nvim.setLine('foo') let times = 0 let disposable = workspace.registerAutocmd({ event: ['CursorMoved'], request: true, callback: () => { times++ } }) nvim.command('doautocmd CursorMoved', true) await helper.waitValue(() => times, 1) disposable.dispose() await nvim.command('doautocmd CursorMoved') await helper.wait(10) expect(times).toBe(1) }) it('should not throw on autocmd callback error', async () => { let called = false let disposable = workspace.registerAutocmd({ event: 'CursorHold', request: false, callback: () => { called = true throw new Error('my error') } }) nvim.command('doautocmd CursorHold', true) await helper.waitValue(() => called, true) disposable.dispose() }) it('should setup user autocmd', async () => { let called = false workspace.registerAutocmd({ event: 'User CocJumpPlaceholder', callback: () => { called = true } }) await nvim.command('doautocmd User CocJumpPlaceholder') await helper.waitValue(() => called, true) }) }) describe('doAutocmd()', () => { it('should not throw when command id does not exist', async () => { await workspace.autocmds.doAutocmd(999, []) }) it('should cancel timeout request autocmd', async () => { let cancelled = false workspace.autocmds.registerAutocmd({ event: 'CursorMoved,CursorMovedI', request: true, callback: (token: CancellationToken) => { return new Promise(resolve => { let timer = setTimeout(() => { resolve() }, 5000) token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) resolve() }) }) }, stack: '' }) let autocmds = workspace.autocmds.autocmds let keys = autocmds.keys() let max = Math.max(...Array.from(keys)) await workspace.autocmds.doAutocmd(max, [], 10) expect(cancelled).toBe(true) }) it('should dispose', async () => { workspace.autocmds.dispose() }) }) ================================================ FILE: src/__tests__/core/documents.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import { LocationLink, Position, Range, TextEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import Documents from '../../core/documents' import events from '../../events' import languages from '../../languages' import BufferSync from '../../model/bufferSync' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let documents: Documents let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim documents = workspace.documentsManager }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) afterAll(async () => { await helper.shutdown() }) describe('BufferSync', () => { it('should recreate document', async () => { let doc = documents.getDocument(documents.bufnr) let called = false let sync = new BufferSync(doc => { return { bufnr: doc.bufnr, dispose: () => { called = true } } }, documents) sync.create(doc) expect(called).toBe(true) }) }) describe('documents', () => { it('should convert filetype', () => { const shouldConvert = (from: string, to: string): void => { expect(documents.convertFiletype(from)).toBe(to) } shouldConvert('javascript.jsx', 'javascriptreact') shouldConvert('typescript.jsx', 'typescriptreact') shouldConvert('typescript.tsx', 'typescriptreact') shouldConvert('tex', 'latex') Object.assign(documents['_env']['filetypeMap'], { foo: 'bar' }) shouldConvert('foo', 'bar') }) it('should get document', async () => { await helper.createDocument('bar') let doc = await helper.createDocument('foo') let res = documents.getDocument(doc.uri) expect(res.uri).toBe(doc.uri) let uri = 'file:///' + doc.uri.slice(8).toUpperCase() res = documents.getDocument(uri, true) expect(res.uri).toBe(doc.uri) res = documents.getDocument(uri, false) expect(res).toBeNull() }) it('should resolveRoot', async () => { let res = documents.resolveRoot(['package.json']) expect(res).toBeDefined() expect(() => { documents.resolveRoot(['unexpected file'], true) }).toThrow(Error) await helper.edit(__filename) res = documents.resolveRoot(['package.json']) expect(res).toBeDefined() }) it('should consider lisp option for iskeyword', async () => { await nvim.command(`e +setl\\ lisp t`) let doc = await workspace.document expect(doc.isWord('-')).toBe(true) }) it('should get languageId', async () => { await helper.createDocument('t.vim') expect(documents.getLanguageId('/a/b')).toBe('') expect(documents.getLanguageId('/a/b.vim')).toBe('vim') expect(documents.getLanguageId('/a/b.c')).toBe('') }) it('should get lines', async () => { let doc = await helper.createDocument('tmp') await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) let lines = await documents.getLines(doc.uri) expect(lines).toEqual(['foo', 'bar']) lines = await documents.getLines('lsptest:///1') expect(lines).toEqual([]) lines = await documents.getLines('file:///not_exists_file') expect(lines).toEqual([]) let uri = URI.file(__filename).toString() lines = await documents.getLines(uri) expect(lines.length).toBeGreaterThan(0) }) it('should read empty string from none file', async () => { let res = await documents.readFile('test:///1') expect(res).toBe('') }) it('should get empty line from none file', async () => { let res = await documents.getLine('test:///1', 1) expect(res).toBe('') let uri = URI.file(path.join(__dirname, 'not_exists_file')).toString() res = await documents.getLine(uri, 1) expect(res).toBe('') }) it('should convert filepath', () => { Object.assign((documents as any)._env, { isCygwin: true, unixPrefix: '/cygdrive/' }) let filepath = documents.fixUnixPrefix('C:\\Users\\Local') expect(filepath).toBe('/cygdrive/c/Users/Local') Object.assign((documents as any)._env, { isCygwin: false }) }) it('should get QuickfixItem from location link', async () => { let doc = await helper.createDocument('quickfix') let loc = LocationLink.create(doc.uri, Range.create(0, 0, 3, 0), Range.create(0, 0, 0, 3)) let res = await documents.getQuickfixItem(loc, 'text', 'E', 'module') expect(res.targetRange).toBeDefined() expect(res.type).toBe('E') expect(res.module).toBe('module') expect(res.bufnr).toBe(doc.bufnr) }) it('should create document', async () => { await helper.createDocument() let bufnrs = await nvim.call('coc#ui#open_files', [[__filename]]) as number[] let bufnr = bufnrs[0] let doc = workspace.getDocument(bufnr) expect(doc).toBeUndefined() doc = await documents.createDocument(bufnr) expect(doc).toBeDefined() }) it('should check buffer rename on save', async () => { let doc = await workspace.document let bufnr = doc.bufnr let name = `${uuid()}.vim` let tmpfile = path.join(os.tmpdir(), name) await nvim.command(`write ${tmpfile}`) doc = workspace.getDocument(bufnr) expect(doc).toBeDefined() expect(doc.filetype).toBe('vim') expect(doc.bufname).toMatch(name) fs.unlinkSync(tmpfile) }) it('should get current document', async () => { let p1 = workspace.document let p2 = workspace.document let arr = await Promise.all([p1, p2]) expect(arr[0]).toBe(arr[1]) }) it('should get bufnrs', async () => { await workspace.document let bufnrs = Array.from(documents.bufnrs) expect(bufnrs.length).toBe(1) }) it('should get uri', async () => { let doc = await workspace.document expect(documents.uri).toBe(doc.uri) }) it('should get current uri', async () => { let doc = await workspace.document documents.detachBuffer(doc.bufnr) let uri = await documents.getCurrentUri() expect(uri).toBeUndefined() }) it('should attach events on vim', async () => { await documents.attach(nvim, workspace.env) let env = Object.assign(workspace.env, { isVim: true }) documents.detach() await documents.attach(nvim, env) documents.detach() await events.fire('CursorMoved', [1, [1, 1]]) }) it('should compute word ranges', async () => { expect(await workspace.computeWordRanges('file:///1', Range.create(0, 0, 1, 0))).toBeNull() let doc = await workspace.document expect(await workspace.computeWordRanges(doc.uri, Range.create(0, 0, 1, 0))).toBeDefined() }) it('should try code actions', async () => { helper.updateConfiguration('editor.codeActionsOnSave', { 'source.fixAll': false }, disposables) let doc = await workspace.document let res = await documents.tryCodeActionsOnSave(doc) expect(res).toBe(false) helper.updateConfiguration('editor.codeActionsOnSave', { 'source.fixAll.eslint': true, 'source.organizeImports': 'always' }, disposables) res = await documents.tryCodeActionsOnSave(doc) expect(res).toBe(true) }) it('should not fire document event when filetype not changed', async () => { let fn = jest.fn() disposables.push(documents.onDidOpenTextDocument(e => { fn() })) let doc = await workspace.document doc.setFiletype('javascript') documents.onFileTypeChange('javascript', doc.bufnr) await helper.wait(10) expect(fn).toHaveBeenCalledTimes(0) doc.detach() documents.onFileTypeChange('javascript', doc.bufnr) await helper.wait(10) expect(fn).toHaveBeenCalledTimes(0) }) it('should fire document create once on reload', async () => { await helper.createDocument('t.vim') let called = false disposables.push(documents.onDidOpenTextDocument(e => { called = true })) await nvim.command('edit') await helper.waitValue(() => called, true) }) }) describe('formatOnSave', () => { it('should not throw when provider not found', async () => { helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['javascript'], disposables) let filepath = await createTmpFile('') await helper.edit(filepath) await nvim.command('setf javascript') await nvim.setLine('foo') await nvim.command('silent w') }) it('should invoke format on save', async () => { helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['text'], disposables) disposables.push(languages.registerDocumentFormatProvider(['text'], { provideDocumentFormattingEdits: document => { let lines = document.getText().replace(/\n$/, '').split(/\n/) let edits: TextEdit[] = [] for (let i = 0; i < lines.length; i++) { let text = lines[i] if (!text.startsWith(' ')) { edits.push(TextEdit.insert(Position.create(i, 0), ' ')) } } return edits } })) let filepath = await createTmpFile('a\nb\nc\n') let buf = await helper.edit(filepath) let doc = workspace.getDocument(buf.id) doc.setFiletype('text') await documents.tryFormatOnSave(doc) let lines = await buf.lines expect(lines).toEqual([' a', ' b', ' c']) }) it('should cancel when timeout', async () => { helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['*'], disposables) let timer disposables.push(languages.registerDocumentFormatProvider(['*'], { provideDocumentFormattingEdits: () => { return new Promise(resolve => { timer = setTimeout(() => { resolve(undefined) }, 2000) }) } })) let filepath = await createTmpFile('a\nb\nc\n') await helper.edit(filepath) let n = Date.now() await nvim.command('w') expect(Date.now() - n).toBeLessThan(1000) clearTimeout(timer) }) it('should enable format on save', async () => { helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', null) helper.updateConfiguration('coc.preferences.formatOnSave', true) let doc = await workspace.document let res = documents.shouldFormatOnSave(doc) expect(res).toBe(false) disposables.push(languages.registerDocumentFormatProvider(['*'], { provideDocumentFormattingEdits: () => { return [] } })) res = documents.shouldFormatOnSave(doc) expect(res).toBe(true) }) it('should not format on save when disabled', async () => { helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['text']) disposables.push(languages.registerDocumentFormatProvider(['text'], { provideDocumentFormattingEdits: document => { let lines = document.getText().replace(/\n$/, '').split(/\n/) let edits: TextEdit[] = [] for (let i = 0; i < lines.length; i++) { edits.push(TextEdit.insert(Position.create(0, 0), ' ')) } return edits } })) let filepath = await createTmpFile('a\nb\nc\n') nvim.pauseNotification() nvim.command('e ' + filepath, true) nvim.command('let b:coc_disable_autoformat = 1', true) nvim.command('setf text', true) await nvim.resumeNotification() await nvim.command('w') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual(['a', 'b', 'c']) }) }) ================================================ FILE: src/__tests__/core/editors.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import Editors, { TextEditor, renamed } from '../../core/editors' import events from '../../events' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let editors: Editors let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim editors = workspace.editors }) afterEach(async () => { await helper.reset() }) afterAll(async () => { disposeAll(disposables) await helper.shutdown() }) describe('util', () => { it('should check renamed', async () => { await helper.edit('foo') let editor = editors.activeTextEditor expect(renamed(editor, { bufnr: 0, fullpath: '', tabid: 1, winid: 1000, })).toBe(false) expect(renamed(editor, { bufnr: editor.document.bufnr, fullpath: '', tabid: 1, winid: 1000, })).toBe(true) expect(renamed(editor, { bufnr: editor.document.bufnr, fullpath: URI.parse(editor.document.uri).fsPath, tabid: 1, winid: 1000, })).toBe(false) Object.assign(editor, { uri: 'lsp:///1' }) expect(renamed(editor, { bufnr: editor.document.bufnr, fullpath: '', tabid: 1, winid: 1000, })).toBe(false) }) }) describe('editors', () => { function assertEditor(editor: TextEditor, tabpagenr: number, winid: number) { expect(editor).toBeDefined() expect(editor.tabpageid).toBe(tabpagenr) expect(editor.winid).toBe(winid) } it('should have active editor', async () => { let winid = await nvim.call('win_getid') as number let editor = window.activeTextEditor assertEditor(editor, 1, winid) let editors = window.visibleTextEditors expect(editors.length).toBe(1) workspace.editors.checkTabs([]) workspace.editors.checkUnloadedBuffers([]) }) it('should get winids of bufnr', () => { let res = workspace.editors.getBufWinids(1000) expect(res).toEqual([]) }) it('should create editor not created', async () => { await nvim.command(`edit +setl\\ buftype=nofile foo`) let doc = await workspace.document await nvim.command('setl buftype=') await events.fire('BufDetach', [doc.bufnr]) await events.fire('CursorHold', [doc.bufnr]) expect(window.activeTextEditor).toBeDefined() expect(window.visibleTextEditors.length).toBe(1) }) it('should detect buffer rename', async () => { let doc = await helper.createDocument('foo') await doc.buffer.setName('bar') await events.fire('CursorHold', [doc.bufnr]) expect(window.activeTextEditor).toBeDefined() expect(window.activeTextEditor.id).toMatch(/bar$/) }) it('should detect buffer switch', async () => { let doc = await helper.createDocument('foo') await helper.createDocument('bar') await nvim.command('noa b ' + doc.bufnr) await events.fire('CursorHold', [doc.bufnr]) expect(window.activeTextEditor).toBeDefined() expect(window.activeTextEditor.id).toMatch(/foo$/) }) it('should change active editor on split', async () => { let promise = new Promise(resolve => { editors.onDidChangeActiveTextEditor(e => { resolve(e) }, null, disposables) }) await nvim.command('vnew') let editor = await promise let winid = await nvim.call('win_getid') expect(editor.winid).toBe(winid) }) it('should change active editor on tabe', async () => { let promise = new Promise(resolve => { editors.onDidChangeActiveTextEditor(e => { if (e.document.uri.includes('foo')) { resolve(e) } }, null, disposables) }) await nvim.command('tabe a | tabe b | tabe foo') let editor = await promise let winid = await nvim.call('win_getid') expect(editor.winid).toBe(winid) }) it('should change active editor on edit', async () => { await nvim.call('win_getid') let n = 0 let promise = new Promise(resolve => { window.onDidChangeVisibleTextEditors(() => { n++ }, null, disposables) editors.onDidChangeActiveTextEditor(e => { n++ resolve(e) }) }) await nvim.command('edit editors') let editor = await promise expect(editor.document.uri).toMatch('editors') await helper.waitValue(() => { return n >= 2 }, true) }) it('should change active editor on window switch', async () => { let winid = await nvim.call('win_getid') await nvim.command('vs foo') await nvim.command('wincmd p') let curr = editors.activeTextEditor expect(curr.winid).toBe(winid) expect(editors.visibleTextEditors.length).toBe(2) }) it('should cleanup on CursorHold', async () => { let promise = new Promise(resolve => { editors.onDidChangeActiveTextEditor(e => { if (e.document.uri.includes('foo')) { resolve(e) } }, null, disposables) }) await nvim.command('sp foo') await promise await nvim.command('noa close') let bufnr = await nvim.eval("bufnr('%')") await events.fire('CursorHold', [bufnr]) expect(editors.visibleTextEditors.length).toBe(1) }) it('should cleanup on create', async () => { let winid = await nvim.call('win_getid') let promise = new Promise(resolve => { editors.onDidChangeActiveTextEditor(e => { if (e.document.uri.includes('foo')) { resolve(e) } }, null, disposables) }) await nvim.command('tabe foo') await promise await nvim.call('win_execute', [winid, 'noa close']) await nvim.command('edit bar') }) it('should have current tabpageid after tab changed', async () => { await nvim.command('tabe|doautocmd CursorHold') await helper.waitValue(() => { return editors.visibleTextEditors.length }, 2) let ids: number[] = [] editors.visibleTextEditors.forEach(editor => { ids.push(editor.tabpageid) }) let editor = editors.visibleTextEditors[editors.visibleTextEditors.length - 1] let previousId = editor.tabpageid await nvim.command('normal! 1gt') await nvim.command('tabe') await helper.waitValue(() => { return editors.visibleTextEditors.length }, 3) expect(editor.tabpageid).toBe(previousId) let tid: number let disposable = editors.onDidTabClose(id => { tid = id }) await nvim.command('tabc') await helper.waitValue(() => { return editors.visibleTextEditors.length }, 2) disposable.dispose() expect(editor.tabpageid).toBe(previousId) expect(tid).toBeDefined() editor = editors.visibleTextEditors.find(o => o.tabpageid == tid) expect(editor).toBeUndefined() }) it('should recreate editor on document reload', async () => { let doc = await helper.createDocument('foo') let bufnr = doc.bufnr await nvim.command('edit!') await helper.waitValue(() => { return workspace.getDocument(bufnr) !== doc }, true) doc = workspace.getDocument(bufnr) expect(editors.activeTextEditor.document.bufnr).toBe(bufnr) expect(editors.activeTextEditor.document === doc).toBe(true) await nvim.command('setf javascript') await helper.waitValue(() => { return doc.filetype }, 'javascript') expect(editors.activeTextEditor.document.filetype).toBe('javascript') }) }) describe('Tabs', () => { it('should attach tabs', async () => { let doc = await workspace.document expect(workspace.tabs.isActive(doc.textDocument)).toBe(true) expect(workspace.tabs.isActive(URI.parse(doc.uri))).toBe(true) expect(workspace.tabs.isVisible(doc.textDocument)).toBe(true) expect(workspace.tabs.isVisible(URI.parse(doc.uri))).toBe(true) workspace.editors['winid'] = 1 expect(workspace.tabs.isActive(URI.parse(doc.uri))).toBe(false) let resources = workspace.tabs.getTabResources() expect(resources.size).toBeGreaterThan(0) }) it('should fire open and close event', async () => { let tabs = workspace.tabs let fn = jest.fn() let disposable = tabs.onOpen(() => { fn() }) nvim.command('tabe foo', true) nvim.command('tabe foo', true) await helper.waitValue(() => { return tabs.getTabResources().size }, 2) disposable.dispose() expect(fn).toHaveBeenCalledTimes(1) nvim.command('bd', true) fn = jest.fn() disposable = tabs.onClose(() => { fn() }) await helper.waitValue(() => { return tabs.getTabResources().size }, 1) disposable.dispose() expect(fn).toHaveBeenCalledTimes(1) }) }) ================================================ FILE: src/__tests__/core/fileSystemWatcher.test.ts ================================================ import bser from 'bser' import net from 'net' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import Configurations from '../../configuration/index' import { FileSystemWatcher, FileSystemWatcherManager } from '../../core/fileSystemWatcher' import Watchman, { FileChangeItem } from '../../core/watchman' import WorkspaceFolderController from '../../core/workspaceFolder' import RelativePattern from '../../model/relativePattern' import { GlobPattern } from '../../types' import { disposeAll } from '../../util' import { remove } from '../../util/fs' import helper from '../helper' let server: net.Server let client: net.Socket const cwd = path.resolve(__dirname, '../../..') const sockPath = path.join(os.tmpdir(), `watchman-fake-${uuid()}`) process.env.WATCHMAN_SOCK = sockPath let workspaceFolder: WorkspaceFolderController let watcherManager: FileSystemWatcherManager let configurations: Configurations let disposables: Disposable[] = [] function wait(ms: number): Promise { return new Promise(resolve => { setTimeout(() => { resolve(undefined) }, ms) }) } function createFileChange(file: string, isNew = true, exists = true): FileChangeItem { return { size: 1, name: file, exists, new: isNew, type: 'f', mtime_ms: Date.now() } } function sendResponse(data: any): void { client.write(bser.dumpToBuffer(data)) } function sendSubscription(uid: string, root: string, files: FileChangeItem[]): void { client.write(bser.dumpToBuffer({ subscription: uid, root, files })) } let capabilities: any let watchResponse: any let defaultConfig = { watchmanPath: null, enable: true, ignoredFolders: [] } beforeAll(async () => { await helper.setup() }) beforeAll(done => { let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json') configurations = new Configurations(userConfigFile, undefined) workspaceFolder = new WorkspaceFolderController(configurations) watcherManager = new FileSystemWatcherManager(workspaceFolder, defaultConfig) Object.assign(watcherManager, { disabled: false }) watcherManager.attach(helper.createNullChannel()) // create a mock sever for watchman server = net.createServer(c => { client = c c.on('data', data => { let obj = bser.loadFromBuffer(data) if (obj[0] == 'watch-project') { sendResponse(watchResponse || { watch: obj[1], warning: 'warning' }) } else if (obj[0] == 'unsubscribe') { sendResponse({ path: obj[1] }) } else if (obj[0] == 'clock') { sendResponse({ clock: 'clock' }) } else if (obj[0] == 'version') { let { optional, required } = obj[1] let res = {} for (let key of optional) { res[key] = true } for (let key of required) { res[key] = true } sendResponse({ capabilities: capabilities || res }) } else if (obj[0] == 'subscribe') { sendResponse({ subscribe: obj[2] }) } else { sendResponse({}) } }) }) server.on('error', err => { throw err }) server.listen(sockPath, () => { done() }) server.unref() }) afterEach(async () => { disposeAll(disposables) capabilities = undefined watchResponse = undefined }) afterAll(async () => { await helper.shutdown() watcherManager.dispose() server.close() await remove(sockPath) }) describe('watchman', () => { it('should not throw error when not watching', async () => { let client = new Watchman(null) disposables.push(client) let disposable = client.subscribe('**/*', () => {}) disposable.dispose() client.dispose() }) it('should checkCapability', async () => { let client = new Watchman(null) let res = await client.checkCapability() expect(res).toBe(true) capabilities = { relative_root: false } res = await client.checkCapability() expect(res).toBe(false) client.dispose() }) it('should watchProject', async () => { let client = new Watchman(null) disposables.push(client) let res = await client.watchProject(__dirname) expect(res).toBe(true) client.dispose() }) it('should unsubscribe', async () => { let client = new Watchman(null) disposables.push(client) await client.watchProject(cwd) let fn = jest.fn() let disposable = client.subscribe(`${cwd}/*`, fn) disposable.dispose() client.dispose() }) }) describe('Watchman#subscribe', () => { it('should subscribe file change', async () => { let client = new Watchman(null, helper.createNullChannel()) disposables.push(client) await client.watchProject(cwd) let called = false let disposable = client.subscribe(`${cwd}/*`, () => { called = true }) let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)] sendSubscription(client.subscription, cwd, changes) await helper.wait(30) expect(called).toBe(true) disposable.dispose() client.dispose() }) it('should subscribe with relative_path', async () => { let client = new Watchman(null, helper.createNullChannel()) watchResponse = { watch: cwd, relative_path: 'foo' } await client.watchProject(cwd) let fn = jest.fn() let disposable = client.subscribe(`${cwd}/*`, fn) let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)] sendSubscription(client.subscription, cwd, changes) await wait(30) expect(fn).toHaveBeenCalled() let call = fn.mock.calls[0][0] disposable.dispose() expect(call.root).toBe(path.join(cwd, 'foo')) client.dispose() }) it('should not subscribe invalid response', async () => { let c = new Watchman(null, helper.createNullChannel()) disposables.push(c) watchResponse = { watch: cwd, relative_path: 'foo' } await c.watchProject(cwd) let fn = jest.fn() c.subscribe(`${cwd}/*`, fn) let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)] sendSubscription('uuid', cwd, changes) await wait(10) sendSubscription(c.subscription, cwd, []) await wait(10) client.write(bser.dumpToBuffer({ subscription: c.subscription, root: cwd })) await wait(10) expect(fn).toHaveBeenCalledTimes(0) }) }) describe('Watchman#createClient', () => { it('should not create client when capabilities not match', async () => { capabilities = { relative_root: false } await expect(async () => { await Watchman.createClient(null, cwd) }).rejects.toThrow(Error) }) it('should not create when watch failed', async () => { watchResponse = {} await expect(async () => { await Watchman.createClient(null, cwd) }).rejects.toThrow(Error) }) it('should create client', async () => { let client = await Watchman.createClient(null, cwd) disposables.push(client) expect(client).toBeDefined() }) }) describe('fileSystemWatcher', () => { async function createWatcher(pattern: GlobPattern, ignoreCreateEvents = false, ignoreChangeEvents = false, ignoreDeleteEvents = false): Promise { let watcher = watcherManager.createFileSystemWatcher( pattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents ) disposables.push(watcher) return watcher } beforeAll(async () => { workspaceFolder.addWorkspaceFolder(cwd, true) await watcherManager.waitClient(cwd) }) it('should use relative pattern #1', async () => { let folder = workspaceFolder.workspaceFolders[0] expect(folder).toBeDefined() let pattern = new RelativePattern(folder, '**/*') let watcher = await createWatcher(pattern, false, true, true) let fn = jest.fn() watcher.onDidCreate(fn) let changes: FileChangeItem[] = [createFileChange(`a`)] sendSubscription(watcher.subscribe, cwd, changes) await helper.wait(30) expect(fn).toHaveBeenCalled() }) it('should use relative pattern #2', async () => { let called = false let pattern = new RelativePattern(__dirname, '**/*') let watcher = await createWatcher(pattern, false, true, true) watcher.onDidCreate(() => { called = true }) let changes: FileChangeItem[] = [createFileChange(`a`)] sendSubscription(watcher.subscribe, cwd, changes) await helper.wait(30) expect(called).toBe(false) }) it('should use relative pattern #3', async () => { let called = false let root = path.join(process.cwd(), 'not_exists') let pattern = new RelativePattern(root, '**/*') let watcher = await createWatcher(pattern, false, true, true) watcher.onDidCreate(() => { called = true }) await helper.wait(10) let changes: FileChangeItem[] = [createFileChange(`a`)] sendSubscription(watcher.subscribe, cwd, changes) await helper.wait(10) expect(called).toBe(false) }) it('should watch for file create', async () => { let watcher = await createWatcher('**/*', false, true, true) let called = false watcher.onDidCreate(() => { called = true }) let changes: FileChangeItem[] = [createFileChange(`a`)] sendSubscription(watcher.subscribe, cwd, changes) await helper.waitValue(() => { return called }, true) }) it('should watch for file delete', async () => { let watcher = await createWatcher('**/*', true, true, false) let called = false watcher.onDidDelete(() => { called = true }) let changes: FileChangeItem[] = [createFileChange(`a`, false, false)] sendSubscription(watcher.subscribe, cwd, changes) await helper.waitValue(() => { return called }, true) }) it('should watch for file change', async () => { let watcher = await createWatcher('**/*', false, false, false) let called = false watcher.onDidChange(() => { called = true }) let changes: FileChangeItem[] = [createFileChange(`a`, false, true)] sendSubscription(watcher.subscribe, cwd, changes) await helper.waitValue(() => { return called }, true) }) it('should watch for file rename', async () => { let watcher = await createWatcher('**/*', false, false, false) let called = false watcher.onDidRename(() => { called = true }) await helper.wait(50) let changes: FileChangeItem[] = [ createFileChange(`a`, false, false), createFileChange(`b`, true, true), ] sendSubscription(watcher.subscribe, cwd, changes) await helper.waitValue(() => { return called }, true) }) it('should not watch for events', async () => { let watcher = await createWatcher('**/*', true, true, true) let called = false let onChange = () => { called = true } watcher.onDidCreate(onChange) watcher.onDidChange(onChange) watcher.onDidDelete(onChange) let changes: FileChangeItem[] = [ createFileChange(`a`, false, false), createFileChange(`b`, true, true), createFileChange(`c`, false, true), ] sendSubscription(watcher.subscribe, cwd, changes) await helper.wait(10) expect(called).toBe(false) }) it('should watch for folder rename', async () => { let watcher = await createWatcher('**/*') let newFiles: string[] = [] let count = 0 watcher.onDidRename(e => { count++ newFiles.push(e.newUri.fsPath) }) let changes: FileChangeItem[] = [ createFileChange(`a/1`, false, false), createFileChange(`a/2`, false, false), createFileChange(`b/1`, true, true), createFileChange(`b/2`, true, true), ] sendSubscription(watcher.subscribe, cwd, changes) await helper.waitValue(() => { return count }, 2) }) it('should watch for new folder', async () => { let watcher = await createWatcher('**/*') expect(watcher).toBeDefined() workspaceFolder.renameWorkspaceFolder(cwd, __dirname) let uri: URI watcher.onDidCreate(e => { uri = e }) await watcherManager.waitClient(__dirname) let changes: FileChangeItem[] = [createFileChange(`a`)] sendSubscription(watcher.subscribe, __dirname, changes) await helper.waitValue(() => { return uri?.fsPath }, path.join(__dirname, 'a')) }) }) describe('create FileSystemWatcherManager', () => { it('should attach to existing workspace folder', async () => { let workspaceFolder = new WorkspaceFolderController(configurations) workspaceFolder.addWorkspaceFolder(cwd, false) let watcherManager = new FileSystemWatcherManager(workspaceFolder, { ...defaultConfig, enable: false }) watcherManager.disabled = false watcherManager.attach(helper.createNullChannel()) await watcherManager.createClient(cwd) await watcherManager.waitClient(cwd) watcherManager.dispose() }) it('should get watchman path', async () => { let watcherManager = new FileSystemWatcherManager(workspaceFolder, { ...defaultConfig, watchmanPath: 'invalid_command' }) process.env.WATCHMAN_SOCK = '' await expect(async () => { await watcherManager.getWatchmanPath() }).rejects.toThrow(Error) process.env.WATCHMAN_SOCK = sockPath }) }) ================================================ FILE: src/__tests__/core/files.test.ts ================================================ import { Buffer, Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol' import { CreateFile, DeleteFile, Position, Range, RenameFile, SnippetTextEdit, StringValue, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../../commands' import events from '../../events' import { getOriginalLine, RecoverFunc } from '../../model/editInspect' import RelativePattern from '../../model/relativePattern' import { disposeAll } from '../../util' import { readFile } from '../../util/fs' import window from '../../window' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) disposables = [] }) describe('RelativePattern', () => { function testThrow(fn: () => void) { let err try { fn() } catch (e) { err = e } expect(err).toBeDefined() } it('should throw for invalid arguments', async () => { testThrow(() => { new RelativePattern('', undefined) }) testThrow(() => { new RelativePattern({ uri: undefined } as any, '') }) }) it('should create relativePattern', async () => { for (let base of [__filename, URI.file(__filename), { uri: URI.file(__dirname).toString(), name: 'test' }]) { let p = new RelativePattern(base, '**/*') expect(URI.isUri(p.baseUri)).toBe(true) expect(p.toJSON()).toBeDefined() } }) }) describe('findFiles()', () => { beforeEach(() => { workspace.workspaceFolderControl.setWorkspaceFolders([__dirname]) }) it('should use glob pattern', async () => { let res = await workspace.findFiles('**/*.ts', undefined, 1) expect(res.length).toBeGreaterThan(0) }) it('should use relativePattern', async () => { let relativePattern = new RelativePattern(URI.file(__dirname), '**/*.ts') let res = await workspace.findFiles(relativePattern) expect(res.length).toBeGreaterThan(0) }) it('should respect exclude as glob pattern', async () => { let arr = await workspace.findFiles('**/*.ts', 'files*') let res = arr.find(o => path.relative(__dirname, o.fsPath).startsWith('files')) expect(res).toBeUndefined() }) it('should respect exclude as relativePattern', async () => { let relativePattern = new RelativePattern(URI.file(__dirname), 'files*') let arr = await workspace.findFiles('**/*.ts', relativePattern) let res = arr.find(o => path.relative(__dirname, o.fsPath).startsWith('files')) expect(res).toBeUndefined() relativePattern = new RelativePattern(URI.file(path.join(__dirname, 'foo')), '**/*.ts') arr = await workspace.findFiles('**/*.ts', relativePattern, 1) expect(arr.length).toBe(1) }) it('should respect maxResults', async () => { let arr = await workspace.findFiles('**/*.ts', undefined, 1) expect(arr.length).toBe(1) }) it('should respect token', async () => { let source = new CancellationTokenSource() source.cancel() let arr = await workspace.findFiles('**/*.ts', undefined, 2, source.token) expect(arr.length).toBe(0) }) it('should cancel findFiles', async () => { let source = new CancellationTokenSource() let p = workspace.findFiles('**/*.ts', undefined, undefined, source.token) setTimeout(() => { source.cancel() }, 10) let arr = await p expect(arr).toBeDefined() }) }) describe('applyEdits()', () => { it('should not throw when unable to undo & redo', async () => { await commands.executeCommand('workspace.undo') await commands.executeCommand('workspace.redo') }) it('should throw for unsupported scheme', () => { expect(() => { let edit = TextDocumentEdit.create({ uri: 'lsp:/1', version: 1 }, [TextEdit.insert(Position.create(0, 0), ' ')]) workspace.files.validateChanges([edit]) }).toThrow(Error) expect(() => { let edit = TextDocumentEdit.create({ uri: 'lsp:/1', version: null }, [TextEdit.insert(Position.create(0, 0), ' ')]) workspace.files.validateChanges([edit]) }).toThrow(Error) let rename = RenameFile.create('lsp:/1', 'lsp:/2') expect(() => { workspace.files.validateChanges([rename]) }).toThrow(Error) }) it('should show error when document with version not loaded', async () => { let uri = 'lsptest:///file' let versioned = VersionedTextDocumentIdentifier.create(uri, 1) let edit = TextEdit.insert(Position.create(0, 0), 'bar') let change = TextDocumentEdit.create(versioned, [edit]) let workspaceEdit: WorkspaceEdit = { documentChanges: [change] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(false) let line = await helper.getCmdline() expect(line).toMatch('Error') }) it('should apply TextEdit of documentChanges', async () => { let doc = await helper.createDocument() let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version) let edit = TextEdit.insert(Position.create(0, 0), 'bar') let change = TextDocumentEdit.create(versioned, [edit]) let workspaceEdit: WorkspaceEdit = { documentChanges: [change] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) let line = await nvim.getLine() expect(line).toBe('bar') await nvim.command('bd!') await workspace.files.undoWorkspaceEdit() }) it('should apply edit with out change buffers', async () => { let doc = await helper.createDocument() await nvim.setLine('bar') await doc.synchronize() let version = doc.version let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version) let edit = TextEdit.replace(Range.create(0, 0, 0, 3), 'bar') let change = TextDocumentEdit.create(versioned, [edit]) let workspaceEdit: WorkspaceEdit = { documentChanges: [change] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) expect(doc.version).toBe(version) }) it('should apply snippet edits', async () => { let filepath = await createTmpFile('foo\nbar\n') let doc = await helper.createDocument(filepath) let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version) let edit = TextEdit.insert(Position.create(0, 0), 'before\n') let snippetEdit: SnippetTextEdit = { range: Range.create(2, 0, 2, 0), snippet: StringValue.createSnippet('after($1)') } let change = TextDocumentEdit.create(versioned, [edit, snippetEdit]) let workspaceEdit: WorkspaceEdit = { documentChanges: [change] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) let newLines = doc.textDocument.lines expect(newLines).toEqual(['before', 'foo', 'bar', 'after()']) await workspace.files.undoWorkspaceEdit() newLines = doc.textDocument.lines expect(newLines).toEqual(['foo', 'bar']) }) it('should not apply TextEdit if version miss match', async () => { let doc = await helper.createDocument() let versioned = VersionedTextDocumentIdentifier.create(doc.uri, 10) let edit = TextEdit.insert(Position.create(0, 0), 'bar') let change = TextDocumentEdit.create(versioned, [edit]) let workspaceEdit: WorkspaceEdit = { documentChanges: [change] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(false) }) it('should apply edits with changes to buffer', async () => { let doc = await helper.createDocument() let changes = { [doc.uri]: [TextEdit.insert(Position.create(0, 0), 'bar')] } let workspaceEdit: WorkspaceEdit = { changes } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) let line = await nvim.getLine() expect(line).toBe('bar') }) it('should apply edits with changes to file not in buffer list', async () => { let filepath = await createTmpFile('bar') let uri = URI.file(filepath).toString() let changes = { [uri]: [TextEdit.insert(Position.create(0, 0), 'foo')] } let res = await workspace.applyEdit({ changes }) expect(res).toBe(true) let doc = workspace.getDocument(uri) let content = doc.getDocumentContent() expect(content).toMatch(/^foobar/) await nvim.command('silent! %bwipeout!') }) it('should apply edits when file does not exist', async () => { let filepath = path.join(__dirname, 'not_exists') disposables.push({ dispose: () => { if (fs.existsSync(filepath)) { fs.unlinkSync(filepath) } } }) let uri = URI.file(filepath).toString() let changes = { [uri]: [TextEdit.insert(Position.create(0, 0), 'foo')] } let res = await workspace.applyEdit({ changes }) expect(res).toBe(true) }) it('should adjust cursor position after applyEdits', async () => { let doc = await helper.createDocument() let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 0 }) let edit = TextEdit.insert(Position.create(0, 0), 'foo\n') let versioned = VersionedTextDocumentIdentifier.create(doc.uri, null) let documentChanges = [TextDocumentEdit.create(versioned, [edit])] let res = await workspace.applyEdit({ documentChanges }) expect(res).toBe(true) pos = await window.getCursorPosition() expect(pos).toEqual({ line: 1, character: 0 }) }) it('should throw when waitUntil is not synchronize', async () => { let err workspace.onWillCreateFiles(e => { setTimeout(() => { try { e.waitUntil(Promise.resolve()) } catch (e) { err = e } }, 0) }, null, disposables) let file = path.join(os.tmpdir(), uuid()) await workspace.createFile(file, { overwrite: true }) expect(err).toBeDefined() fs.rmSync(file, { force: true }) }) it('should support null version of documentChanges', async () => { let file = path.join(__dirname, 'foo') await workspace.createFile(file, { ignoreIfExists: true, overwrite: true }) let uri = URI.file(file).toString() let versioned = VersionedTextDocumentIdentifier.create(uri, null) let edit = TextEdit.insert(Position.create(0, 0), 'bar') let change = TextDocumentEdit.create(versioned, [edit]) let workspaceEdit: WorkspaceEdit = { documentChanges: [change] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) await nvim.command('wa') let content = await readFile(file, 'utf8') expect(content).toMatch(/^bar/) await workspace.deleteFile(file, { ignoreIfNotExists: true }) }) it('should support CreateFile edit', async () => { let file = path.join(__dirname, 'foo') let uri = URI.file(file).toString() let workspaceEdit: WorkspaceEdit = { documentChanges: [CreateFile.create(uri, { overwrite: true })] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) await workspace.deleteFile(file, { ignoreIfNotExists: true }) }) it('should support DeleteFile edit', async () => { let file = path.join(__dirname, 'foo') await workspace.createFile(file, { ignoreIfExists: true, overwrite: true }) let uri = URI.file(file).toString() let workspaceEdit: WorkspaceEdit = { documentChanges: [DeleteFile.create(uri)] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) }) it('should check uri for CreateFile edit', async () => { let workspaceEdit: WorkspaceEdit = { documentChanges: [CreateFile.create('term://.', { overwrite: true })] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(false) }) it('should support RenameFile edit', async () => { let file = path.join(__dirname, 'foo') await workspace.createFile(file, { ignoreIfExists: true, overwrite: true }) let newFile = path.join(__dirname, 'bar') let uri = URI.file(file).toString() let workspaceEdit: WorkspaceEdit = { documentChanges: [RenameFile.create(uri, URI.file(newFile).toString())] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) await workspace.deleteFile(newFile, { ignoreIfNotExists: true }) }) it('should support changes with edit and rename', async () => { let fsPath = await createTmpFile('test') let doc = await helper.createDocument(fsPath) let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`) let newUri = URI.file(newFile).toString() let edit: WorkspaceEdit = { documentChanges: [ { textDocument: { version: null, uri: doc.uri, }, edits: [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } }, newText: 'bar' } ] }, { oldUri: doc.uri, newUri, kind: 'rename' } ] } let res = await workspace.applyEdit(edit) expect(res).toBe(true) await nvim.call('cursor', [1, 1]) let curr = await workspace.document expect(curr.uri).toBe(newUri) expect(curr.getline(0)).toBe('bar') let line = await nvim.line expect(line).toBe('bar') }) it('should support edit new file with CreateFile', async () => { let file = path.join(os.tmpdir(), uuid()) let uri = URI.file(file).toString() let workspaceEdit: WorkspaceEdit = { documentChanges: [ CreateFile.create(uri, { overwrite: true }), TextDocumentEdit.create({ uri, version: 0 }, [ TextEdit.insert(Position.create(0, 0), 'foo bar') ]) ] } let res = await workspace.applyEdit(workspaceEdit) expect(res).toBe(true) let doc = workspace.getDocument(uri) expect(doc).toBeDefined() let line = doc.getline(0) expect(line).toBe('foo bar') await workspace.deleteFile(file, { ignoreIfNotExists: true }) }) it('should undo and redo workspace edit', async () => { const folder = path.join(os.tmpdir(), uuid()) const pathone = path.join(folder, 'a') const pathtwo = path.join(folder, 'b') await workspace.files.createFile(pathone, { overwrite: true }) await workspace.files.createFile(pathtwo, { overwrite: true }) let uris = [URI.file(pathone).toString(), URI.file(pathtwo).toString()] const assertContent = (one: string, two: string) => { let doc = workspace.getDocument(uris[0]) expect(doc.getDocumentContent()).toBe(one) doc = workspace.getDocument(uris[1]) expect(doc.getDocumentContent()).toBe(two) } let edits: TextDocumentEdit[] = [] edits.push(TextDocumentEdit.create({ uri: uris[0], version: null }, [ TextEdit.insert(Position.create(0, 0), 'foo') ])) edits.push(TextDocumentEdit.create({ uri: uris[1], version: null }, [ TextEdit.insert(Position.create(0, 0), 'bar') ])) await workspace.applyEdit({ documentChanges: edits }) assertContent('foo\n', 'bar\n') await workspace.files.undoWorkspaceEdit() assertContent('\n', '\n') await workspace.files.redoWorkspaceEdit() assertContent('foo\n', 'bar\n') }) it('should should support annotations', async () => { async function assertEdit(confirm: boolean, description: string | undefined): Promise { let doc = await helper.createDocument(uuid()) let edit: WorkspaceEdit = { documentChanges: [ { textDocument: { version: doc.version, uri: doc.uri }, edits: [ { range: Range.create(0, 0, 0, 0), newText: 'bar', annotationId: '85bc78e2-5ef0-4949-b10c-13f476faf430' } ] }, ], changeAnnotations: { '85bc78e2-5ef0-4949-b10c-13f476faf430': { needsConfirmation: true, label: 'Text changes', description } } } let p = workspace.files.applyEdit(edit) await helper.waitPrompt() if (confirm) { await nvim.input('') } else { await nvim.input('') } await p let content = doc.getDocumentContent() if (confirm) { expect(content).toBe('bar\n') } else { expect(content).toBe('\n') } } await assertEdit(true, 'description') await assertEdit(false, undefined) }) }) describe('getOriginalLine', () => { it('should get original line', async () => { let item = { index: 0, filepath: '' } expect(getOriginalLine(item, undefined)).toBeUndefined() expect(getOriginalLine({ index: 0, filepath: '', lnum: 1 }, undefined)).toBe(1) let doc = await helper.createDocument() let change = { textDocument: { version: doc.version, uri: doc.uri }, edits: [ { range: Range.create(0, 0, 0, 0), newText: 'bar', }, { range: Range.create(2, 0, 2, 0), snippet: StringValue.createSnippet('foo') } ] } expect(getOriginalLine({ index: 0, filepath: '', lnum: 1 }, change)).toBe(1) }) describe('inspectEdit', () => { async function inspect(edit: WorkspaceEdit): Promise { await workspace.applyEdit(edit) await commands.executeCommand('workspace.inspectEdit') let buf = await nvim.buffer return buf } it('should show warning when edit not exists', async () => { (workspace.files as any).editState = undefined await workspace.files.inspectEdit() }) it('should render with changes', async () => { let fsPath = await createTmpFile('foo\n1\n2\nbar') let doc = await helper.createDocument(fsPath) let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`) let newUri = URI.file(newFile).toString() let createFile = path.join(os.tmpdir(), `coc-${process.pid}/create-${uuid()}`) let deleteFile = await createTmpFile('delete') disposables.push(Disposable.create(() => { if (fs.existsSync(newFile)) fs.unlinkSync(newFile) if (fs.existsSync(createFile)) fs.unlinkSync(createFile) if (fs.existsSync(deleteFile)) fs.unlinkSync(deleteFile) })) let edit: WorkspaceEdit = { documentChanges: [ { textDocument: { version: null, uri: doc.uri, }, edits: [ TextEdit.del(Range.create(0, 0, 1, 0)), TextEdit.replace(Range.create(3, 0, 3, 3), 'xyz'), ] }, { kind: 'rename', oldUri: doc.uri, newUri }, { kind: 'create', uri: URI.file(createFile).toString() }, { kind: 'delete', uri: URI.file(deleteFile).toString() } ] } let buf = await inspect(edit) let lines = await buf.lines let content = lines.join('\n') expect(content).toMatch('Change') expect(content).toMatch('Rename') expect(content).toMatch('Create') expect(content).toMatch('Delete') await nvim.command('exe 5') await nvim.input('') await helper.waitFor('expand', ['%:p'], newFile) let line = await nvim.call('line', ['.']) expect(line).toBe(3) }) it('should render annotation label', async () => { let filepath = path.join(__dirname, uuid()) disposables.push(Disposable.create(() => { if (fs.existsSync(filepath)) { fs.unlinkSync(filepath) } })) let doc = await helper.createDocument(filepath) let edit: WorkspaceEdit = { documentChanges: [ { textDocument: { version: doc.version, uri: doc.uri }, edits: [ { range: Range.create(0, 0, 0, 0), newText: 'bar', annotationId: 'dd866f37-a24c-4503-9c35-c139fb28e25b' } ] }, { textDocument: { version: 1, uri: doc.uri }, edits: [ { range: Range.create(0, 0, 0, 0), newText: 'bar', annotationId: '9468b9bf-97b6-4b37-b21f-aba8df3ce658' } ] }], changeAnnotations: { 'dd866f37-a24c-4503-9c35-c139fb28e25b': { needsConfirmation: false, label: 'Text changes' } } } let buf = await inspect(edit) await events.fire('BufUnload', [buf.id + 1]) let winid = await nvim.call('win_getid') let lines = await buf.lines expect(lines[0]).toBe('Text changes') await nvim.command('exe 1') await nvim.command('wa') await nvim.input('') let bufnr = await nvim.call('bufnr', ['%']) expect(bufnr).toBe(buf.id) await nvim.command('exe 3') await nvim.input('') let fsPath = URI.parse(doc.uri).fsPath await helper.waitFor('eval', ['expand("%:p")'], fsPath) await nvim.call('win_gotoid', [winid]) await nvim.input('') await helper.wait(10) }) }) describe('createFile()', () => { it('should create and revert parent folder', async () => { const folder = path.join(os.tmpdir(), uuid()) const filepath = path.join(folder, 'a/b/bar') disposables.push(Disposable.create(() => { fs.rmSync(folder, { recursive: true, force: true }) })) let fns: RecoverFunc[] = [] expect(fs.existsSync(folder)).toBe(false) await workspace.files.createFile(filepath, {}, fns) expect(fs.existsSync(filepath)).toBe(true) for (let i = fns.length - 1; i >= 0; i--) { await fns[i]() } expect(fs.existsSync(folder)).toBe(false) }) it('should throw when file already exists', async () => { let filepath = await createTmpFile('foo', disposables) let fn = async () => { await workspace.createFile(filepath, {}) } await expect(fn()).rejects.toThrow(Error) }) it('should not create file if file exists with ignoreIfExists', async () => { let file = await createTmpFile('foo') await workspace.createFile(file, { ignoreIfExists: true }) let content = fs.readFileSync(file, 'utf8') expect(content).toBe('foo') }) it('should create file if does not exist', async () => { await helper.edit() let filepath = path.join(__dirname, 'foo') await workspace.createFile(filepath, { ignoreIfExists: true }) let exists = fs.existsSync(filepath) expect(exists).toBe(true) fs.unlinkSync(filepath) }) it('should revert file create', async () => { let filepath = path.join(os.tmpdir(), uuid()) disposables.push(Disposable.create(() => { if (fs.existsSync(filepath)) fs.unlinkSync(filepath) })) let fns: RecoverFunc[] = [] await workspace.files.createFile(filepath, { overwrite: true }, fns) expect(fs.existsSync(filepath)).toBe(true) let bufnr = await nvim.call('bufnr', [filepath]) as number expect(bufnr).toBeGreaterThan(0) let doc = workspace.getDocument(bufnr) expect(doc).toBeDefined() for (let fn of fns) { await fn() } expect(fs.existsSync(filepath)).toBe(false) let loaded = await nvim.call('bufloaded', [filepath]) expect(loaded).toBe(0) }) }) describe('renameFile', () => { it('should throw when oldPath not exists', async () => { await workspace.renameFile('/foo', '/foo') await workspace.renameFile('/foo', __filename, { ignoreIfExists: true }) let filepath = path.join(__dirname, 'not_exists_file') let newPath = path.join(__dirname, 'bar') let fn = async () => { await workspace.renameFile(filepath, newPath) } await expect(fn()).rejects.toThrow(Error) }) it('should throw when new path exists and not overwrite', async () => { await expect(async () => { await workspace.renameFile('/foo', __filename, {}) }).rejects.toThrow(/exists/) }) it('should rename file on disk', async () => { let filepath = await createTmpFile('test') let newPath = path.join(path.dirname(filepath), 'new_file') disposables.push(Disposable.create(() => { if (fs.existsSync(newPath)) fs.unlinkSync(newPath) if (fs.existsSync(filepath)) fs.unlinkSync(filepath) })) let fns: RecoverFunc[] = [] await workspace.files.renameFile(filepath, newPath, { overwrite: true }, fns) expect(fs.existsSync(newPath)).toBe(true) for (let fn of fns) { await fn() } expect(fs.existsSync(newPath)).toBe(false) expect(fs.existsSync(filepath)).toBe(true) }) it('should rename if file does not exist', async () => { let filepath = path.join(__dirname, 'foo') let newPath = path.join(__dirname, 'bar') await workspace.createFile(filepath) await workspace.renameFile(filepath, newPath) expect(fs.existsSync(newPath)).toBe(true) expect(fs.existsSync(filepath)).toBe(false) fs.unlinkSync(newPath) }) it('should rename current buffer with same bufnr', async () => { let file = await createTmpFile('test') let doc = await helper.createDocument(file) await nvim.setLine('bar') await doc.patchChange() let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`) disposables.push(Disposable.create(() => { if (fs.existsSync(newFile)) fs.unlinkSync(newFile) })) await workspace.renameFile(file, newFile) let bufnr = await nvim.call('bufnr', ['%']) expect(bufnr).toBe(doc.bufnr) let line = await nvim.line expect(line).toBe('bar') let exists = fs.existsSync(newFile) expect(exists).toBe(true) }) it('should overwrite if file exists', async () => { let filepath = path.join(os.tmpdir(), uuid()) let newPath = path.join(os.tmpdir(), uuid()) await workspace.createFile(filepath) await workspace.createFile(newPath) await workspace.renameFile(filepath, newPath, { overwrite: true }) expect(fs.existsSync(newPath)).toBe(true) expect(fs.existsSync(filepath)).toBe(false) fs.unlinkSync(newPath) }) it('should rename buffer in directory and revert', async () => { let folder = path.join(os.tmpdir(), uuid()) let newFolder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder) disposables.push(Disposable.create(() => { fs.rmSync(folder, { recursive: true, force: true }) fs.rmSync(newFolder, { recursive: true, force: true }) })) let filepath = path.join(folder, 'new_file') await workspace.createFile(filepath) let bufnr = await nvim.call('bufnr', [filepath]) expect(bufnr).toBeGreaterThan(0) let fns: RecoverFunc[] = [] await workspace.files.renameFile(folder, newFolder, { overwrite: true }, fns) bufnr = await nvim.call('bufnr', [path.join(newFolder, 'new_file')]) expect(bufnr).toBeGreaterThan(0) for (let i = fns.length - 1; i >= 0; i--) { await fns[i]() } bufnr = await nvim.call('bufnr', [filepath]) expect(bufnr).toBeGreaterThan(0) }) }) describe('loadResource()', () => { it('should load file as hidden buffer', async () => { helper.updateConfiguration('workspace.openResourceCommand', '') let filepath = await createTmpFile('foo') let uri = URI.file(filepath).toString() let doc = await workspace.files.loadResource(uri) let bufnrs = await nvim.call('coc#window#bufnrs') as number[] expect(bufnrs.indexOf(doc.bufnr)).toBe(-1) }) }) describe('deleteFile()', () => { it('should throw when file not exists', async () => { let filepath = path.join(__dirname, 'not_exists') let fn = async () => { await workspace.deleteFile(filepath) } await expect(fn()).rejects.toThrow(Error) }) it('should ignore when ignoreIfNotExists set', async () => { let filepath = path.join(__dirname, 'not_exists') let fns: RecoverFunc[] = [] await workspace.files.deleteFile(filepath, { ignoreIfNotExists: true }, fns) expect(fns.length).toBe(0) }) it('should unload loaded buffer', async () => { let filepath = await createTmpFile('file to delete') disposables.push(Disposable.create(() => { if (fs.existsSync(filepath)) fs.unlinkSync(filepath) })) await workspace.files.loadResource(URI.file(filepath).toString()) let fns: RecoverFunc[] = [] await workspace.files.deleteFile(filepath, {}, fns) let loaded = await nvim.call('bufloaded', [filepath]) expect(loaded).toBe(0) for (let i = fns.length - 1; i >= 0; i--) { await fns[i]() } expect(fs.existsSync(filepath)).toBe(true) loaded = await nvim.call('bufloaded', [filepath]) expect(loaded).toBe(1) }) it('should delete and recover folder', async () => { let folder = path.join(os.tmpdir(), uuid()) disposables.push(Disposable.create(() => { if (fs.existsSync(folder)) fs.rmdirSync(folder) })) fs.mkdirSync(folder) expect(fs.existsSync(folder)).toBe(true) let fns: RecoverFunc[] = [] await workspace.files.deleteFile(folder, {}, fns) expect(fs.existsSync(folder)).toBe(false) for (let i = fns.length - 1; i >= 0; i--) { await fns[i]() } expect(fs.existsSync(folder)).toBe(true) await workspace.files.deleteFile(folder, {}) }) it('should delete and recover folder recursive', async () => { let folder = path.join(os.tmpdir(), uuid()) disposables.push(Disposable.create(() => { fs.rmSync(folder, { recursive: true, force: true }) })) fs.mkdirSync(folder) fs.writeFileSync(path.join(folder, 'new_file'), '', 'utf8') let fns: RecoverFunc[] = [] await workspace.files.deleteFile(folder, { recursive: true }, fns) expect(fs.existsSync(folder)).toBe(false) for (let i = fns.length - 1; i >= 0; i--) { await fns[i]() } expect(fs.existsSync(folder)).toBe(true) expect(fs.existsSync(path.join(folder, 'new_file'))).toBe(true) await workspace.files.deleteFile(folder, { recursive: true }) }) it('should delete file if exists', async () => { let filepath = path.join(__dirname, 'foo') await workspace.createFile(filepath) expect(fs.existsSync(filepath)).toBe(true) await workspace.deleteFile(filepath) expect(fs.existsSync(filepath)).toBe(false) }) }) describe('loadFile()', () => { it('should single loadFile', async () => { let doc = await helper.createDocument() let newFile = URI.file(path.join(__dirname, 'abc')).toString() let document = await workspace.loadFile(newFile) let bufnr = await nvim.call('bufnr', '%') expect(document.uri.endsWith('abc')).toBe(true) expect(bufnr).toBe(doc.bufnr) }) }) describe('loadFiles', () => { it('should loadFiles', async () => { let files = ['a', 'b', 'c'].map(key => URI.file(path.join(__dirname, key)).toString()) let docs = await workspace.loadFiles(files) let uris = docs.map(o => o.uri) expect(uris).toEqual(files) await workspace.loadFiles([]) }) it('should load uri', async () => { let res = await workspace.loadFiles(['deno:/foo']) expect(res[0].uri).toBe('deno:/foo') }) }) describe('openTextDocument()', () => { it('should open document already exists', async () => { let doc = await helper.createDocument('a') await nvim.command('enew') await workspace.openTextDocument(URI.parse(doc.uri)) let curr = await workspace.document expect(curr.uri != doc.uri).toBe(true) }) it('should throw when file does not exist', async () => { await expect(async () => { await workspace.openTextDocument('/a/b/c') }).rejects.toThrow(Error) }) it('should open untitled document', async () => { let doc = await workspace.openTextDocument(URI.parse(`untitled:///a/b.js`)) expect(doc.uri).toBe('file:///a/b.js') }) it('should load file that exists', async () => { let doc = await workspace.openTextDocument(URI.file(__filename)) expect(URI.parse(doc.uri).fsPath).toBe(__filename) }) }) }) ================================================ FILE: src/__tests__/core/funcs.test.ts ================================================ import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import which from 'which' import Configurations from '../../configuration/index' import * as funcs from '../../core/funcs' import Resolver from '../../model/resolver' let configurations: Configurations beforeAll(async () => { let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json') configurations = new Configurations(userConfigFile, undefined) }) describe('Resolver()', () => { it('should return empty string when file not exists', async () => { let spy = jest.spyOn(fs, 'existsSync').mockImplementation(() => { return false }) let r = new Resolver() let res = await r.yarnFolder expect(res).toBe('') spy.mockRestore() }) it('should resolve null', async () => { let r = new Resolver() let spy = jest.spyOn(which, 'sync').mockImplementation(() => { throw new Error('not found') }) let res = await r.resolveModule('mode') expect(res).toBe(null) spy.mockRestore() }) it('should resolve npm module', async () => { let r = new Resolver() let folder = path.join(os.tmpdir(), uuid()) Object.assign(r, { _npmFolder: folder, _yarnFolder: __dirname, }) fs.mkdirSync(path.join(folder, 'name'), { recursive: true }) fs.writeFileSync(path.join(folder, 'name', 'package.json'), '', 'utf8') let res = await r.resolveModule('name') expect(res).toBe(path.join(folder, 'name')) }) }) describe('has()', () => { it('should throw for invalid argument', async () => { let env = { isVim: true, version: '8023956' } let err try { expect(funcs.has(env, '0.5.0')).toBe(true) } catch (e) { err = e } expect(err).toBeDefined() }) it('should detect version on vim8', async () => { let env = { isVim: true, version: '8023956' } expect(funcs.has(env, 'patch-7.4.248')).toBe(true) expect(funcs.has(env, 'patch-8.5.1')).toBe(false) expect(funcs.has(env, 'patch-9.0.0125')).toBe(false) }) it('should delete version on neovim', async () => { let env = { isVim: false, version: '0.6.1' } expect(funcs.has(env, 'nvim-0.5.0')).toBe(true) expect(funcs.has(env, 'nvim-0.7.0')).toBe(false) }) }) describe('createNameSpace()', () => { it('should create namespace', async () => { let nr = funcs.createNameSpace('ns') expect(nr).toBeDefined() expect(nr).toBe(funcs.createNameSpace('ns')) }) }) describe('getWatchmanPath()', () => { it('should get watchman path', async () => { let res = funcs.getWatchmanPath(configurations) expect(typeof res === 'string' || res == null).toBe(true) configurations.updateMemoryConfig({ 'coc.preferences.watchmanPath': 'not_exists_watchman' }) res = funcs.getWatchmanPath(configurations) expect(res).toBeNull() configurations.updateMemoryConfig({ 'coc.preferences.watchmanPath': null }) }) }) describe('findUp()', () => { it('should return null when can not find ', async () => { let nvim: any = { call: () => { return __filename } } let res = await funcs.findUp(nvim, os.homedir(), ['file_not_exists']) expect(res).toBeNull() }) it('should return null when unable find cwd in cwd', async () => { let nvim: any = { call: () => { return '' } } let res = await funcs.findUp(nvim, os.homedir(), ['file_not_exists']) expect(res).toBeNull() }) }) describe('score()', () => { it('should return score', () => { expect(funcs.score(undefined, 'untitled:///1', '')).toBe(0) expect(funcs.score({ scheme: '*' }, 'untitled:///1', '')).toBe(3) expect(funcs.score('vim', 'untitled:///1', 'vim')).toBe(10) expect(funcs.score('*', 'untitled:///1', '')).toBe(5) expect(funcs.score('', 'untitled:///1', 'vim')).toBe(0) expect(funcs.score({ pattern: '/*' }, 'untitled:///1', 'vim', false)).toBe(5) expect(funcs.score({ pattern: { pattern: '/*', baseUri: '/tmp' } }, 'untitled:///1', 'vim', false)).toBe(0) expect(funcs.score({ pattern: { pattern: '/**', baseUri: '/tmp' } }, 'file:///tmp/a/b', 'vim')).toBe(5) expect(funcs.score({ pattern: { pattern: '/**', baseUri: '/tmp' } }, 'file:///foo', 'vim')).toBe(0) }) }) ================================================ FILE: src/__tests__/core/keymaps.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import Keymaps, { getBufnr, getKeymapModifier } from '../../core/keymaps' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let keymaps: Keymaps let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim keymaps = workspace.keymaps }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('doKeymap()', () => { it('should not throw when key not mapped', async () => { await keymaps.doKeymap('', '') }) it('should invoke exists keymap', async () => { let called = false keymaps.registerKeymap(['i', 'n'], 'test-keymap', () => { called = true return 'result' }) let res = await keymaps.doKeymap('test-keymap', '') expect(res).toBe('result') expect(called).toBe(true) }) }) describe('registerKeymap()', () => { it('should getBufnr', () => { expect(getBufnr(3)).toBe(3) expect(getBufnr(true)).toBe(0) }) it('should getKeymapModifier', () => { expect(getKeymapModifier('i', true)).toBe('') expect(getKeymapModifier('i')).toBe('') expect(getKeymapModifier('s')).toBe('') expect(getKeymapModifier('x')).toBe('') expect(getKeymapModifier('t')).toBe('') }) it('should throw for invalid key', () => { let err try { keymaps.registerKeymap(['i'], '', jest.fn()) } catch (e) { err = e } expect(err).toBeDefined() }) it('should throw for duplicated key', async () => { keymaps.registerKeymap(['i'], 'tmp', jest.fn()) let err try { keymaps.registerKeymap(['i'], 'tmp', jest.fn()) } catch (e) { err = e } expect(err).toBeDefined() }) it('should register insert key mapping', async () => { let fn = jest.fn() disposables.push(keymaps.registerKeymap(['i'], 'test', fn)) let res = await nvim.call('execute', ['verbose imap (coc-test)']) expect(res).toMatch('coc#_insert_key') }) it('should register with different options', async () => { let called = false let fn = () => { called = true return '' } disposables.push(keymaps.registerKeymap(['n', 'v'], 'test', fn, { sync: false, cancel: false, silent: false, repeat: true })) let res = await nvim.exec(`verbose nmap (coc-test)`, true) expect(res).toMatch('coc#rpc#notify') await nvim.eval(`feedkeys("\\(coc-test)")`) await helper.waitValue(() => called, true) }) }) describe('registerExprKeymap()', () => { it('should visual key mapping', async () => { await nvim.setLine('foo') let called = false let fn = () => { called = true return '' } disposables.push(keymaps.registerExprKeymap('x', 'x', fn)) await nvim.command('normal! viw') await nvim.input('x') await helper.waitValue(() => called, true) }) it('should register expr insert key mapping', async () => { let buf = await nvim.buffer let called = false let fn = () => { called = true return '' } let disposable = keymaps.registerExprKeymap('i', 'x', fn, buf.id) let res = await nvim.exec('imap x', true) expect(res).toMatch('coc#_insert_key') await nvim.input('i') await nvim.input('x') await helper.waitValue(() => called, true) disposable.dispose() res = await nvim.exec('imap x', true) expect(res).toMatch('No mapping found') }) it('should regist key mapping without cancel pum', async () => { let fn = jest.fn() let disposable = keymaps.registerExprKeymap('i', 'x', fn, false, false) let res = await nvim.exec('imap x', true) expect(res).toMatch('coc#_insert_key') disposable.dispose() }) }) describe('registerLocalKeymap', () => { it('should register local keymap by notification', async () => { let bufnr = await nvim.call('bufnr', ['%']) as number let called = false let disposable = keymaps.registerLocalKeymap(bufnr, 'n', 'n', () => { called = true return '' }, true) let res = await nvim.exec('nmap n', true) await nvim.input('n') await helper.waitValue(() => called, true) disposable.dispose() res = await nvim.exec('nmap n', true) expect(res).toMatch('No mapping found') }) it('should regist insert mode keymap', async () => { let bufnr = await nvim.call('bufnr', ['%']) as number await nvim.command('startinsert') let called = false let disposable = keymaps.registerLocalKeymap(bufnr, 'i', '', () => { called = true }, { cancel: true }) disposables.push(disposable) await helper.waitValue(async () => { let out = await nvim.exec('imap ', true) return out.includes('coc#_insert_key') }, true) await nvim.input('') await helper.waitValue(() => called, true) called = false disposable = keymaps.registerLocalKeymap(bufnr, 'i', '', () => { called = true }, { cancel: false }) disposables.push(disposable) await helper.waitValue(async () => { let out = await nvim.exec('imap ', true) return out.includes('coc#_insert_key') }, true) await nvim.input('') await helper.waitValue(() => called, true) }) it('should regist cmd keymap', async () => { let bufnr = await nvim.call('bufnr', ['%']) as number let called = false let disposable = keymaps.registerLocalKeymap(bufnr, 'x', '', async () => { called = true }, { cmd: true }) disposables.push(disposable) await nvim.setLine('foo') await nvim.command('normal! v$') let m = await nvim.mode expect(m.mode).toBe('v') await nvim.input('') await helper.waitValue(() => called, true) m = await nvim.mode expect(m.mode).toBe('c') }) }) ================================================ FILE: src/__tests__/core/locations.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import os from 'os' import path from 'path' import { Location, Position, Range } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import commands from '../../commands' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim await nvim.command(`source ${path.join(process.cwd(), 'autoload/coc/ui.vim')}`) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) function createLocations(): Location[] { let uri = URI.file(__filename).toString() return [Location.create(uri, Range.create(0, 0, 1, 0)), Location.create(uri, Range.create(2, 0, 3, 0))] } describe('showLocations()', () => { it('should show locations by editor.action.showReferences', async () => { let doc = await workspace.document let uri = doc.uri let locations = createLocations() await commands.executeCommand('editor.action.showReferences', uri, Position.create(0, 0), locations) await helper.waitValue(async () => { let wins = await nvim.windows return wins.length > 1 }, true) }) it('should show location list by default', async () => { let locations = createLocations() await workspace.showLocations(locations) await helper.waitFor('bufname', ['%'], 'list:///location') }) it('should fire autocmd when location list disabled', async () => { Object.assign(workspace.env, { locationlist: false }) await nvim.exec(` function OnLocationsChange() let g:called = 1 endfunction autocmd User CocLocationsChange :call OnLocationsChange()`) let locations = createLocations() await workspace.showLocations(locations) await helper.waitFor('eval', [`get(g:,'called',0)`], 1) }) it('should show quickfix when quickfix enabled', async () => { helper.updateConfiguration('coc.preferences.useQuickfixForLocations', true) let locations = createLocations() await workspace.showLocations(locations) await helper.waitFor('eval', [`&buftype`], 'quickfix') }) it('should use customized quickfix open command', async () => { await nvim.setVar('coc_quickfix_open_command', 'copen 1') helper.updateConfiguration('coc.preferences.useQuickfixForLocations', true) let locations = createLocations() await workspace.showLocations(locations) await helper.waitFor('eval', [`&buftype`], 'quickfix') let win = await nvim.window let height = await win.height expect(height).toBe(1) }) }) describe('jumpTo()', () => { it('should jumpTo position', async () => { let uri = URI.file('/tmp/foo') await workspace.jumpTo(uri, { line: 1, character: 1 }) await nvim.command('setl buftype=nofile') let buf = await nvim.buffer let name = await buf.name expect(name).toMatch('/foo') await buf.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) await workspace.jumpTo(uri, { line: 1, character: 1 }) let pos = await nvim.call('getcurpos') as number[] expect(pos.slice(1, 3)).toEqual([2, 2]) }) it('should jumpTo uri without normalize', async () => { let uri = 'zipfile:///tmp/clojure-1.9.0.jar::clojure/core.clj' await workspace.jumpTo(uri) let buf = await nvim.buffer let name = await buf.name expect(name).toBe(uri) let doc = await workspace.document expect(doc.uri.startsWith('zipfile:/tmp')).toBe(true) }) it('should jump without position', async () => { let uri = URI.file('/tmp/foo').toString() await workspace.jumpTo(uri) let buf = await nvim.buffer let name = await buf.name expect(name).toMatch('/foo') }) it('should jumpTo custom uri scheme', async () => { let uri = 'jdt://foo' await workspace.jumpTo(uri, { line: 1, character: 1 }) let buf = await nvim.buffer let name = await buf.name expect(name).toBe(uri) }) it('should jump with uri fragment', async () => { let uri = URI.file(__filename).with({ fragment: '3,3' }).toString() await workspace.jumpTo(uri) let cursor = await nvim.call('coc#util#cursor') expect(cursor).toEqual([2, 2]) uri = URI.file(__filename).with({ fragment: '1' }).toString() await workspace.jumpTo(uri) cursor = await nvim.call('coc#util#cursor') expect(cursor).toEqual([0, 0]) }) }) describe('openResource()', () => { it('should open resource', async () => { let uri = URI.file(path.join(os.tmpdir(), 'bar')).toString() await workspace.openResource(uri) let buf = await nvim.buffer let name = await buf.name expect(name).toMatch('bar') }) it('should open none file uri', async () => { workspace.registerTextDocumentContentProvider('jd', { provideTextDocumentContent: () => 'jd' }) let uri = 'jd://abc' await workspace.openResource(uri) let buf = await nvim.buffer let name = await buf.name expect(name).toBe('jd://abc') }) it('should open opened buffer', async () => { let buf = await helper.edit() let doc = workspace.getDocument(buf.id) await workspace.openResource(doc.uri) await helper.waitFor('bufnr', ['%'], buf.id) }) it('should open url', async () => { await helper.mockFunction('coc#ui#open_url', 0) let buf = await helper.edit() let uri = 'http://example.com' await workspace.openResource(uri) await helper.waitFor('bufnr', ['%'], buf.id) }) }) ================================================ FILE: src/__tests__/core/terminals.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import which from 'which' import Terminals from '../../core/terminals' import { TerminalModel } from '../../model/terminal' import window from '../../window' import helper from '../helper' let nvim: Neovim let terminals: Terminals beforeAll(async () => { await helper.setup() nvim = helper.nvim terminals = new Terminals() }) afterEach(async () => { terminals.reset() await helper.reset() }) afterAll(async () => { await helper.shutdown() }) describe('create terminal', () => { it('should use cleaned env', async () => { let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash'), strictEnv: true }) await helper.wait(10) terminal.sendText(`echo $NODE_ENV`, true) await helper.wait(50) let buf = nvim.createBuffer(terminal.bufnr) let lines = await buf.lines expect(lines.includes('test')).toBe(false) }) it('should use custom shell command', async () => { let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash') }) let bufnr = terminal.bufnr let bufname = await nvim.call('bufname', [bufnr]) as string expect(bufname.includes('bash')).toBe(true) }) it('should use custom cwd', async () => { let basename = path.basename(os.tmpdir()) let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, cwd: os.tmpdir() }) let bufnr = terminal.bufnr let bufname = await nvim.call('bufname', [bufnr]) as string expect(bufname.includes(basename)).toBe(true) }) it('should have exit code', async () => { let exitStatus terminals.onDidCloseTerminal(terminal => { exitStatus = terminal.exitStatus }) let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash'), strictEnv: true }) terminal.sendText('exit', true) await helper.waitFor('bufloaded', [terminal.bufnr], 0) await helper.waitValue(() => { return exitStatus != null }, true) expect(exitStatus.code).toBeDefined() }) it('should return false on show when buffer unloaded', async () => { let model = new TerminalModel('bash', [], nvim) await model.start() expect(model.bufnr).toBeDefined() await nvim.command(`bd! ${model.bufnr}`) let res = await model.show() expect(res).toBe(false) }) it('should not throw when show & hide disposed terminal', async () => { let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash') }) terminal.dispose() await terminal.show() await terminal.hide() }) it('should show terminal on current window', async () => { let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash') }) let winid = await nvim.call('bufwinid', [terminal.bufnr]) expect(winid).toBeGreaterThan(0) await nvim.call('win_gotoid', [winid]) await terminal.show() }) it('should show terminal that shown', async () => { let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash') }) let res = await terminal.show(true) expect(res).toBe(true) }) it('should show hidden terminal', async () => { let terminal = await terminals.createTerminal(nvim, { name: `test-${uuid()}`, shellPath: which.sync('bash') }) await terminal.hide() await terminal.show() }) it('should create terminal', async () => { let terminal = await window.createTerminal({ name: `test-${uuid()}`, }) expect(terminal).toBeDefined() expect(terminal.processId).toBeDefined() expect(terminal.name).toBeDefined() terminal.dispose() await helper.wait(30) expect(terminal.bufnr).toBeUndefined() }) }) ================================================ FILE: src/__tests__/core/ui.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Position, Range } from 'vscode-languageserver-types' import * as ui from '../../core/ui' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('getCursorPosition()', () => { it('should get cursor position', async () => { await nvim.call('cursor', [1, 1]) let res = await ui.getCursorPosition(nvim) expect(res).toEqual({ line: 0, character: 0 }) }) }) describe('moveTo()', () => { it('should moveTo position', async () => { await nvim.setLine('foo') await ui.moveTo(nvim, Position.create(0, 1), true) let res = await ui.getCursorPosition(nvim) expect(res).toEqual({ line: 0, character: 1 }) }) }) describe('getCursorScreenPosition()', () => { it('should get cursor screen position', async () => { let res = await ui.getCursorScreenPosition(nvim) expect(res).toBeDefined() expect(typeof res.row).toBe('number') expect(typeof res.col).toBe('number') }) }) describe('createFloatFactory()', () => { it('should create FloatFactory', async () => { let f = ui.createFloatFactory(nvim, { border: true, autoHide: false, breaks: false }, { close: true }) await f.show([{ content: 'shown', filetype: 'txt' }]) let activated = await f.activated() expect(activated).toBe(true) expect(f.window != null).toBe(true) let win = await helper.getFloat() expect(win).toBeDefined() let id = await nvim.call('coc#float#get_related', [win.id, 'border', 0]) as number expect(id).toBeGreaterThan(0) id = await nvim.call('coc#float#get_related', [win.id, 'close', 0]) as number expect(id).toBeGreaterThan(0) await f.show([{ content: 'shown', filetype: 'txt' }], { offsetX: 10 }) let curr = await helper.getFloat() expect(curr.id).toBe(win.id) }) }) describe('showMessage()', () => { it('should showMessage on vim', async () => { ui.echoMessages(nvim, 'my message', 'more', 'more') await helper.wait(50) let cmdline = await helper.getCmdline() expect(cmdline).toMatch(/my message/) }) it('should get messageLevel', () => { let level = ui.toMessageLevel('error') expect(level).toBe(ui.MessageLevel.Error) level = ui.toMessageLevel('warning') expect(level).toBe(ui.MessageLevel.Warning) level = ui.toMessageLevel('more') expect(level).toBe(ui.MessageLevel.More) }) }) describe('getSelection()', () => { it('should return null when no selection exists', async () => { let res = await ui.getSelection(nvim, 'v') expect(res).toBeNull() }) it('should return range for line selection', async () => { await nvim.setLine('foo') await nvim.input('V') await nvim.input('') let res = await ui.getSelection(nvim, 'V') expect(res).toEqual({ start: { line: 0, character: 0 }, end: { line: 1, character: 0 } }) }) it('should return range of current line', async () => { await nvim.command('normal! gg') let res = await ui.getSelection(nvim, 'currline') expect(res).toEqual(Range.create(0, 0, 1, 0)) }) }) describe('selectRange()', () => { it('should select range #1', async () => { await nvim.call('setline', [1, ['foo', 'b']]) await nvim.command('set selection=inclusive') await nvim.command('set virtualedit=onemore') await ui.selectRange(nvim, Range.create(0, 0, 1, 1), true) await nvim.input('') let res = await ui.getSelection(nvim, 'v') expect(res).toEqual(Range.create(0, 0, 1, 1)) }) it('should select range #2', async () => { await nvim.call('setline', [1, ['foo', 'b']]) await ui.selectRange(nvim, Range.create(0, 0, 1, 0), true) await nvim.input('') let res = await ui.getSelection(nvim, 'v') expect(res).toEqual(Range.create(0, 0, 0, 3)) }) it('should select range #3', async () => { await ui.selectRange(nvim, Range.create(0, 0, 0, 0), true) let m = await nvim.mode expect(m.mode).toBe('v') await nvim.input('') await ui.selectRange(nvim, Range.create(0, 0, 0, 1), true) }) }) ================================================ FILE: src/__tests__/core/workspaceFolder.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { Disposable, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import Configurations from '../../configuration/index' import WorkspaceFolderController, { PatternType } from '../../core/workspaceFolder' import { disposeAll } from '../../util' import { CancellationError } from '../../util/errors' import workspace from '../../workspace' import helper from '../helper' let workspaceFolder: WorkspaceFolderController let configurations: Configurations let disposables: Disposable[] = [] let nvim: Neovim function updateConfiguration(key: string, value: any, defaults: any): void { configurations.updateMemoryConfig({ [key]: value }) disposables.push({ dispose: () => { configurations.updateMemoryConfig({ [key]: defaults }) } }) } beforeAll(async () => { await helper.setup() nvim = helper.nvim let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json') configurations = new Configurations(userConfigFile, undefined) workspaceFolder = new WorkspaceFolderController(configurations) }) afterEach(async () => { await helper.reset() disposeAll(disposables) workspaceFolder.reset() }) afterAll(async () => { await helper.shutdown() }) describe('WorkspaceFolderController', () => { describe('asRelativePath()', () => { function assertAsRelativePath(input: string | URI, expected: string, includeWorkspace?: boolean) { const actual = workspaceFolder.getRelativePath(input, includeWorkspace) expect(actual).toBe(expected) } it('should get relative path', async () => { workspaceFolder.addWorkspaceFolder(`/Coding/Applications/NewsWoWBot`, false) assertAsRelativePath('/Coding/Applications/NewsWoWBot/bernd/das/brot', 'bernd/das/brot') assertAsRelativePath('/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart', '/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart') assertAsRelativePath('', '') assertAsRelativePath('/foo/bar', '/foo/bar') assertAsRelativePath('in/out', 'in/out') assertAsRelativePath(null, '') assertAsRelativePath(URI.file('/tmp'), '/tmp') }) it('should asRelativePath, same paths, #11402', async () => { const root = '/home/aeschli/workspaces/samples/docker' const input = '/home/aeschli/workspaces/samples/docker' workspaceFolder.addWorkspaceFolder(root, false) assertAsRelativePath(input, input) const input2 = '/home/aeschli/workspaces/samples/docker/a.file' assertAsRelativePath(input2, 'a.file') }) it('should asRelativePath, not workspaceFolder', async () => { expect(workspace.asRelativePath('')).toBe('') assertAsRelativePath('/foo/bar', '/foo/bar') }) it('should asRelativePath, multiple folders', () => { workspaceFolder.addWorkspaceFolder(`/Coding/One`, false) workspaceFolder.addWorkspaceFolder(`/Coding/Two`, false) assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt') assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt') assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt') }) it('should slightly inconsistent behaviour of asRelativePath and getWorkspaceFolder, #31553', async () => { workspaceFolder.addWorkspaceFolder(`/Coding/One`, false) workspaceFolder.addWorkspaceFolder(`/Coding/Two`, false) assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt') assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt', true) assertAsRelativePath('/Coding/One/file.txt', 'file.txt', false) assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt') assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt', true) assertAsRelativePath('/Coding/Two/files/out.txt', 'files/out.txt', false) assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt') assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', true) assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', false) }) }) describe('setWorkspaceFolders()', () => { it('should set valid folders', async () => { workspaceFolder.setWorkspaceFolders([os.tmpdir(), '/a/not_exists']) let folders = workspaceFolder.workspaceFolders expect(folders.length).toBe(2) }) }) describe('getWorkspaceFolder()', () => { it('should get workspaceFolder by uri', async () => { let res = workspaceFolder.getWorkspaceFolder(URI.parse('untitled://1')) expect(res).toBeUndefined() res = workspaceFolder.getWorkspaceFolder(URI.file('/a/b')) expect(res).toBeUndefined() let filepath = path.join(process.cwd(), 'a/b') workspaceFolder.setWorkspaceFolders([process.cwd()]) res = workspaceFolder.getWorkspaceFolder(URI.file(filepath)) expect(URI.parse(res.uri).fsPath).toBe(process.cwd()) }) }) describe('getRootPatterns()', () => { it('should get patterns from b:coc_root_patterns', async () => { await nvim.command('edit t.vim | let b:coc_root_patterns=["foo"]') await nvim.command('setf vim') let doc = await workspace.document let res = workspaceFolder.getRootPatterns(doc, PatternType.Buffer) expect(res).toEqual(['foo']) }) it('should add patterns from languageserver', () => { updateConfiguration('languageserver.test', { filetypes: ['vim'], rootPatterns: ['bar'] }, undefined) workspaceFolder.addRootPattern('vim', ['foo']) let res = workspaceFolder.getServerRootPatterns('vim') expect(res.includes('foo')).toBe(true) expect(res.includes('bar')).toBe(true) }) it('should get patterns from user configuration', async () => { let doc = await workspace.document let res = workspaceFolder.getRootPatterns(doc, PatternType.Global) expect(res.includes('.git')).toBe(true) }) }) describe('resolveRoot()', () => { const cwd = process.cwd() const expand = (input: string) => { return workspace.expand(input) } it('should resolve to cwd for file in cwd', async () => { updateConfiguration('workspace.rootPatterns', [], ['.git', '.hg', '.projections.json']) let file = path.join(os.tmpdir(), 'foo') let doc = await helper.createDocument(file) let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand) expect(res).toBe(os.tmpdir()) }) it('should ignore cwd by ignore pattern', async () => { updateConfiguration('workspace.rootPatterns', [], ['.git', '.hg', '.projections.json']) updateConfiguration('workspace.ignoredFolders', ['**/*'], ['$HOME']) let file = path.join(os.tmpdir(), 'foo') let doc = await helper.createDocument(file) let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand) expect(res).toBeNull() }) it('should not fallback to cwd as workspace folder', async () => { updateConfiguration('workspace.rootPatterns', [], ['.git', '.hg', '.projections.json']) updateConfiguration('workspace.workspaceFolderFallbackCwd', false, true) let file = path.join(os.tmpdir(), 'foo') await nvim.command(`edit ${file}`) let doc = await workspace.document let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand) expect(res).toBe(null) }) it('should return null for untitled buffer', async () => { await nvim.command('enew') let doc = await workspace.document let res = workspaceFolder.resolveRoot(doc, cwd, false, expand) expect(res).toBe(null) }) it('should respect ignored filetypes', async () => { updateConfiguration('workspace.ignoredFiletypes', ['vim'], []) await nvim.command('edit t.vim') await nvim.command('setf vim') let doc = await workspace.document let res = workspaceFolder.resolveRoot(doc, cwd, false, expand) expect(res).toBe(null) }) it('should respect workspaceFolderCheckCwd', async () => { let called = 0 disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(() => { called++ })) workspaceFolder.addRootPattern('vim', ['.vim']) await nvim.command('edit a/.vim/t.vim') await nvim.command('setf vim') let doc = await workspace.document let res = workspaceFolder.resolveRoot(doc, cwd, true, expand) expect(res).toBe(process.cwd()) await nvim.command('edit a/foo') doc = await workspace.document res = workspaceFolder.resolveRoot(doc, cwd, true, expand) expect(res).toBe(process.cwd()) expect(called).toBe(1) }) it('should respect ignored folders', async () => { updateConfiguration('workspace.ignoredFolders', ['$HOME/foo', '$HOME'], []) let file = path.join(os.homedir(), '.vim/bar') workspaceFolder.addRootPattern('vim', ['.vim']) await nvim.command(`edit ${file}`) await nvim.command('setf vim') let doc = await workspace.document let res = workspaceFolder.resolveRoot(doc, path.join(os.homedir(), 'foo'), true, expand) expect(res).toBe(null) }) it('should respect specific filetype for bottomUpFileTypes', async () => { updateConfiguration('workspace.rootPatterns', ['.vim'], ['.git', '.hg', '.projections.json']) updateConfiguration('workspace.bottomUpFiletypes', ['vim'], []) let root = path.join(os.tmpdir(), 'a') let dir = path.join(root, '.vim') fs.mkdirSync(dir, { recursive: true }) let file = path.join(dir, 'foo.vim') await nvim.command(`edit ${file}`) let doc = await workspace.document expect(doc.filetype).toBe('vim') let res = workspaceFolder.resolveRoot(doc, file, true, expand) expect(res).toBe(root) }) it('should respect wildcard', async () => { updateConfiguration('workspace.rootPatterns', ['.vim'], ['.git', '.hg', '.projections.json']) updateConfiguration('workspace.bottomUpFiletypes', ['*'], []) let root = path.join(os.tmpdir(), 'a') let dir = path.join(root, '.vim') fs.mkdirSync(dir, { recursive: true }) let file = path.join(dir, 'foo') await nvim.command(`edit ${file}`) let doc = await workspace.document let res = workspaceFolder.resolveRoot(doc, file, true, expand) expect(res).toBe(root) }) }) describe('renameWorkspaceFolder()', () => { it('should rename workspaceFolder', async () => { let e: WorkspaceFoldersChangeEvent disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(ev => { e = ev })) let cwd = process.cwd() workspaceFolder.addWorkspaceFolder(cwd, false) workspaceFolder.addWorkspaceFolder(cwd, false) workspaceFolder.renameWorkspaceFolder(cwd, path.join(cwd, '.vim')) expect(e.removed.length).toBe(1) expect(e.added.length).toBe(1) }) }) describe('removeWorkspaceFolder()', () => { it('should remote workspaceFolder', async () => { let e: WorkspaceFoldersChangeEvent disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(ev => { e = ev })) let cwd = process.cwd() workspaceFolder.addWorkspaceFolder(cwd, false) workspaceFolder.removeWorkspaceFolder(cwd) workspaceFolder.removeWorkspaceFolder('/a/b') expect(e.removed.length).toBe(1) expect(e.added.length).toBe(0) }) it('should not throw for invalid folder', async () => { workspaceFolder.addWorkspaceFolder('tmp', false) workspaceFolder.removeWorkspaceFolder('tmp') workspaceFolder.renameWorkspaceFolder('tmp', 'other') }) }) describe('checkPatterns()', () => { it('should check if pattern exists', async () => { expect(await workspaceFolder.checkPatterns([], ['p'])).toBe(false) let folder: WorkspaceFolder = { name: '', uri: URI.file(process.cwd()).toString() } let res = await workspaceFolder.checkPatterns([folder], ['package.json', '**/not_exists']) expect(res).toBe(true) res = await workspaceFolder.checkPatterns([folder], ['**/not_exists']) expect(res).toBe(false) }) it('should not throw when checkFolder throw error', async () => { let spy = jest.spyOn(workspaceFolder, 'checkFolder').mockImplementation(() => { return Promise.reject(new Error('error')) }) let folder: WorkspaceFolder = { name: '', uri: URI.file(process.cwd()).toString() } await workspaceFolder.checkPatterns([folder], ['package.json', '**/not_exists']) spy.mockRestore() }) it('should not throw on timeout', async () => { let spy = jest.spyOn(workspaceFolder, 'checkFolder').mockImplementation((_dir, _patterns, token) => { return new Promise((resolve, reject) => { let timer = setTimeout(() => { resolve(undefined) }, 200) token.onCancellationRequested(() => { clearTimeout(timer) reject(new CancellationError()) }) }) }) let folder: WorkspaceFolder = { name: '', uri: URI.file(process.cwd()).toString() } let res = await workspaceFolder.checkPatterns([folder], ['**/schema.json']) spy.mockRestore() expect(res).toBe(false) }) }) describe('onDocumentDetach()', () => { it('should check uris', async () => { updateConfiguration('workspace.removeEmptyWorkspaceFolder', true, false) let folder = os.tmpdir() workspaceFolder.addWorkspaceFolder(folder, false) workspaceFolder.onDocumentDetach([URI.parse('untitled:/1'), URI.parse('file:///foo/bar')]) expect(workspaceFolder.workspaceFolders.length).toBe(0) workspaceFolder.addWorkspaceFolder(folder, false) workspaceFolder.onDocumentDetach([URI.parse('untitled:/1'), URI.file(path.join(os.tmpdir(), 'foo'))]) expect(workspaceFolder.workspaceFolders.length).toBe(1) }) }) }) ================================================ FILE: src/__tests__/handler/callHierarchy.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable, CallHierarchyItem, SymbolKind, Range, SymbolTag, CancellationToken, Position } from 'vscode-languageserver-protocol' import CallHierarchyHandler from '../../handler/callHierarchy' import languages from '../../languages' import workspace from '../../workspace' import { disposeAll } from '../../util' import { URI } from 'vscode-uri' import helper, { createTmpFile } from '../helper' import commands from '../../commands' let nvim: Neovim let callHierarchy: CallHierarchyHandler let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim callHierarchy = helper.plugin.getHandler().callHierarchy }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) function createCallItem(name: string, kind: SymbolKind, uri: string, range: Range): CallHierarchyItem { return { name, kind, uri, range, selectionRange: range } } describe('CallHierarchy', () => { it('should throw when provider does not exist', async () => { await expect(async () => { await callHierarchy.getIncoming() }).rejects.toThrow(Error) }) it('should return null when provider not exist', async () => { let token = CancellationToken.None let doc = await workspace.document let res: any res = await languages.prepareCallHierarchy(doc.textDocument, Position.create(0, 0), token) expect(res).toBeNull() let item = createCallItem('name', SymbolKind.Class, doc.uri, Range.create(0, 0, 1, 0)) res = await languages.provideOutgoingCalls(doc.textDocument, item, token) expect(res).toBeNull() res = await languages.provideIncomingCalls(doc.textDocument, item, token) expect(res).toBeNull() }) it('should throw when prepare failed', async () => { disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return undefined }, provideCallHierarchyIncomingCalls() { return [] }, provideCallHierarchyOutgoingCalls() { return [] } })) let fn = async () => { await callHierarchy.getOutgoing() } await expect(fn()).rejects.toThrow(Error) }) it('should get incoming & outgoing callHierarchy items', async () => { disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, 'test:///foo', Range.create(0, 0, 0, 5)) }, provideCallHierarchyIncomingCalls() { return [{ from: createCallItem('bar', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), fromRanges: [Range.create(0, 0, 0, 5)] }] }, provideCallHierarchyOutgoingCalls() { return [{ to: createCallItem('bar', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), fromRanges: [Range.create(1, 0, 1, 5)] }] } })) let res = await helper.doAction('incomingCalls') expect(res.length).toBe(1) expect(res[0].from.name).toBe('bar') let outgoing = await helper.doAction('outgoingCalls') expect(outgoing.length).toBe(1) res = await callHierarchy.getIncoming(outgoing[0].to) expect(res.length).toBe(1) }) it('should show warning when provider does not exist', async () => { await helper.doAction('showIncomingCalls') let line = await helper.getCmdline() expect(line).toMatch('not found') }) it('should show message when no result returned.', async () => { disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return null }, provideCallHierarchyIncomingCalls() { return [] }, provideCallHierarchyOutgoingCalls() { return [] } })) await callHierarchy.showCallHierarchyTree('incoming') let line = await helper.getCmdline() expect(line).toMatch('Unable') }) it('should render description and support default action', async () => { helper.updateConfiguration('callHierarchy.enableTooltip', false) let doc = await workspace.document let bufnr = doc.bufnr await doc.buffer.setLines(['foo'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) }, provideCallHierarchyIncomingCalls() { let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(1, 0, 1, 3)) item.detail = 'Detail' item.tags = [SymbolTag.Deprecated] return [{ from: item, fromRanges: [Range.create(2, 0, 2, 5)] }] }, provideCallHierarchyOutgoingCalls() { return [] } })) await commands.executeCommand('document.showIncomingCalls') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'INCOMING CALLS', '- c foo', ' + c bar Detail' ]) await nvim.command('exe 3') await nvim.input('t') await helper.waitFor('getline', ['.'], ' - c bar Detail') await nvim.input('') await helper.waitFor('expand', ['%:p'], fsPath) let res = await nvim.call('coc#cursor#position') expect(res).toEqual([1, 0]) let matches = await nvim.call('getmatches') as any[] expect(matches.length).toBe(2) await nvim.command(`b ${bufnr}`) await helper.wait(50) matches = await nvim.call('getmatches') as any[] expect(matches.length).toBe(0) await nvim.command(`wincmd o`) }) it('should invoke reveal command', async () => { let doc = await helper.createDocument('foo') await nvim.setLine('foo') let item: any = createCallItem('name', SymbolKind.Class, doc.uri, Range.create(0, 0, 1, 0)) let winid = await nvim.call('win_getid') as number let commandId = 'callHierarchy.reveal' await commands.executeCommand(commandId, winid, item) item.ranges = [Range.create(0, 0, 0, 1)] item.sourceUri = 'lsp:/1' await commands.executeCommand(commandId, winid, item) let newDoc = await helper.createDocument('bar') await workspace.jumpTo(doc.uri) item.sourceUri = newDoc.uri await commands.executeCommand(commandId, winid, item) }) it('should invoke open in new tab action', async () => { let doc = await workspace.document await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) }, provideCallHierarchyIncomingCalls() { return [] }, provideCallHierarchyOutgoingCalls() { let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) item.detail = 'Detail' return [{ to: item, fromRanges: [Range.create(1, 0, 1, 3)] }] } })) let win = await nvim.window await commands.executeCommand('document.showOutgoingCalls') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c foo', ' + c bar Detail' ]) await nvim.command('exe 3') await nvim.input('') await helper.waitPrompt() await nvim.input('') await helper.waitFor('tabpagenr', [], 2) doc = await workspace.document expect(doc.uri).toBe(uri) await helper.waitValue(async () => { let res = await nvim.call('getmatches', [win.id]) as any[] return res.length }, 1) }) it('should invoke show incoming calls action', async () => { let doc = await workspace.document await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) }, provideCallHierarchyIncomingCalls() { return [{ from: createCallItem('test', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), fromRanges: [Range.create(0, 0, 0, 5)] }] }, provideCallHierarchyOutgoingCalls() { let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) item.detail = 'Detail' return [{ to: item, fromRanges: [Range.create(1, 0, 1, 3)] }] } })) await callHierarchy.showCallHierarchyTree('outgoing') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c foo', ' + c bar Detail' ]) await nvim.command('exe 3') await nvim.input('') await helper.waitPrompt() await nvim.input('3') await helper.wait(200) lines = await buf.lines expect(lines).toEqual([ 'INCOMING CALLS', '- c bar Detail', ' + c test' ]) }) it('should invoke show outgoing calls action', async () => { let doc = await workspace.document await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) }, provideCallHierarchyIncomingCalls() { return [{ from: createCallItem('test', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), fromRanges: [Range.create(0, 0, 0, 5)] }] }, provideCallHierarchyOutgoingCalls() { let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) item.detail = 'Detail' return [{ to: item, fromRanges: [Range.create(1, 0, 1, 3)] }] } })) await callHierarchy.showCallHierarchyTree('incoming') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'INCOMING CALLS', '- c foo', ' + c test' ]) await nvim.command('exe 3') await nvim.input('') await helper.waitPrompt() await nvim.input('4') await helper.wait(200) lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c test', ' + c bar Detail' ]) }) it('should invoke dismiss action #1', async () => { let doc = await workspace.document await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) }, provideCallHierarchyIncomingCalls() { return [] }, provideCallHierarchyOutgoingCalls() { let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) item.detail = 'Detail' return [{ to: item, fromRanges: [Range.create(1, 0, 1, 3)] }] } })) await callHierarchy.showCallHierarchyTree('outgoing') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c foo', ' + c bar Detail' ]) await nvim.command('exe 3') await nvim.input('') await helper.waitPrompt() await nvim.input('2') await helper.wait(200) lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c foo' ]) await nvim.command('exe 2') await nvim.input('') await helper.waitPrompt() await nvim.input('2') await helper.wait(30) }) it('should invoke dismiss action #2', async () => { let doc = await workspace.document await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { prepareCallHierarchy() { return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) }, provideCallHierarchyIncomingCalls() { return [] }, provideCallHierarchyOutgoingCalls() { let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) item.detail = 'Detail' return [{ to: item, fromRanges: [Range.create(1, 0, 1, 3)] }] } })) await helper.doAction('showOutgoingCalls') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c foo', ' + c bar Detail' ]) await nvim.command('exe 3') await nvim.input('t') await helper.waitFor('line', ['$'], 4) await nvim.command('exe 4') await nvim.input('') await helper.waitPrompt() await nvim.input('2') await helper.waitFor('line', ['$'], 3) lines = await buf.lines expect(lines).toEqual([ 'OUTGOING CALLS', '- c foo', ' - c bar Detail' ]) }) }) ================================================ FILE: src/__tests__/handler/codeActions.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Command, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import commands from '../../commands' import ActionsHandler, { shouldAutoApply } from '../../handler/codeActions' import languages, { ProviderName } from '../../languages' import { ProviderResult } from '../../provider' import { checkAction } from '../../provider/codeActionManager' import { disposeAll } from '../../util' import { rangeInRange } from '../../util/position' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let codeActions: ActionsHandler let currActions: (CodeAction | Command)[] let resolvedAction: CodeAction beforeAll(async () => { await helper.setup() nvim = helper.nvim codeActions = helper.plugin.getHandler().codeActions }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: ( _document: TextDocument, _range: Range, _context: CodeActionContext, _token: CancellationToken ) => currActions, resolveCodeAction: ( _action: CodeAction, _token: CancellationToken ): ProviderResult => resolvedAction }, undefined)) }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('handler codeActions', () => { describe('autoApply', () => { it('should check auto apply', async () => { expect(shouldAutoApply(undefined)).toBe(false) expect(shouldAutoApply([])).toBe(false) expect(shouldAutoApply([CodeActionKind.Refactor])).toBe(false) }) }) describe('organizeImport', () => { it('should filter command ', () => { let cmd = Command.create('title', 'command') let res = checkAction([CodeActionKind.Refactor], cmd) expect(res).toBe(false) res = checkAction(undefined, cmd) expect(res).toBe(true) }) it('should return false when organize import action not found', async () => { currActions = [] let doc = await helper.createDocument() expect(languages.hasProvider(ProviderName.CodeAction, doc)).toBe(true) let res = await helper.doAction('organizeImport') expect(res).toBe(false) expect(languages.hasProvider('undefined' as any, doc)).toBe(false) }) it('should perform organize import action', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let edits: TextEdit[] = [] edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar')) edits.push(TextEdit.replace(Range.create(1, 0, 1, 3), 'foo')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('organize import', edit, CodeActionKind.SourceOrganizeImports) currActions = [action, CodeAction.create('another action'), Command.create('title', 'command')] await codeActions.organizeImport() let lines = await doc.buffer.lines expect(lines).toEqual(['bar', 'foo']) }) it('should register editor.action.organizeImport command', async () => { let doc = await helper.createDocument() currActions = [] await commands.executeCommand('editor.action.organizeImport') await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) let edits: TextEdit[] = [] edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar')) edits.push(TextEdit.replace(Range.create(1, 0, 1, 3), 'foo')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('organize import', edit, CodeActionKind.SourceOrganizeImports) currActions = [action, CodeAction.create('another action')] await commands.executeCommand('editor.action.organizeImport') let lines = await doc.buffer.lines expect(lines).toEqual(['bar', 'foo']) }) }) describe('codeActionRange', () => { it('should show warning when no action available', async () => { await helper.createDocument() currActions = [] await helper.doAction('codeActionRange', 1, 2, CodeActionKind.QuickFix) let line = await helper.getCmdline() expect(line).toMatch(/No quickfix code action/) await helper.doAction('codeActionRange', 1, 2) line = await helper.getCmdline() expect(line).toMatch(/No code action available/) }) it('should apply chosen action', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) currActions = [action] let p = codeActions.codeActionRange(1, 2, CodeActionKind.QuickFix) await helper.waitPrompt() await nvim.input('') await p let buf = nvim.createBuffer(doc.bufnr) let lines = await buf.lines expect(lines[0]).toBe('bar') }) }) describe('getCodeActions', () => { it('should get empty actions', async () => { currActions = [] let doc = await helper.createDocument() let res = await codeActions.getCodeActions(doc) expect(res.length).toBe(0) }) it('should not filter disabled actions', async () => { currActions = [] let action = CodeAction.create('foo', CodeActionKind.Source) currActions.push(action) action = CodeAction.create('action', CodeActionKind.Empty) currActions.push(action) action = CodeAction.create('bar', CodeActionKind.QuickFix) action.disabled = { reason: 'disabled' } currActions.push(action) let doc = await helper.createDocument() let res = await codeActions.getCodeActions(doc, Range.create(0, 0, 1, 0)) expect(res.length).toBe(2) }) it('should get all actions', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['', '', ''], { start: 0, end: -1, strictIndexing: false }) let action = CodeAction.create('curr action', CodeActionKind.Empty) currActions = [action] let range: Range disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: ( _document: TextDocument, r: Range, _context: CodeActionContext, _token: CancellationToken ) => { range = r return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c'), Command.create('title', 'command')] }, }, undefined)) disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: () => { return [CodeAction.create('a')] }, }, undefined)) let res = await codeActions.getCodeActions(doc) expect(range).toEqual(Range.create(0, 0, 3, 0)) expect(res.length).toBe(5) }) it('should filter actions by range', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['', '', ''], { start: 0, end: -1, strictIndexing: false }) currActions = [] let range: Range disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: ( _document: TextDocument, r: Range, _context: CodeActionContext, _token: CancellationToken ) => { range = r if (rangeInRange(r, Range.create(0, 0, 1, 0))) return [CodeAction.create('a')] return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')] }, }, undefined)) let res = await codeActions.getCodeActions(doc, Range.create(0, 0, 0, 0)) expect(range).toEqual(Range.create(0, 0, 0, 0)) expect(res.length).toBe(1) }) it('should filter actions by kind prefix', async () => { let doc = await helper.createDocument() let action = CodeAction.create('my action', CodeActionKind.SourceFixAll) currActions = [action] let res = await codeActions.getCodeActions(doc, undefined, [CodeActionKind.Source]) expect(res.length).toBe(1) expect(res[0].kind).toBe(CodeActionKind.SourceFixAll) await helper.doAction('fixAll') }) }) describe('getCurrentCodeActions', () => { let range: Range beforeEach(() => { disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: ( _document: TextDocument, r: Range, _context: CodeActionContext, _token: CancellationToken ) => { range = r return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')] }, }, undefined)) }) it('should get codeActions by line', async () => { currActions = [] await helper.createDocument() let res = await helper.doAction('codeActions', 'line') expect(range).toEqual(Range.create(0, 0, 1, 0)) expect(res.length).toBe(3) }) it('should get codeActions by cursor', async () => { currActions = [] await helper.createDocument() let res = await codeActions.getCurrentCodeActions('cursor') expect(range).toEqual(Range.create(0, 0, 0, 0)) expect(res.length).toBe(3) }) it('should get codeActions by visual mode', async () => { currActions = [] await helper.createDocument() await nvim.setLine('foo') await nvim.command('normal! 0v$') await nvim.input('') let res = await codeActions.getCurrentCodeActions('v') expect(range).toEqual(Range.create(0, 0, 0, 3)) expect(res.length).toBe(3) }) }) describe('doCodeAction', () => { it('should not throw when no action exists', async () => { currActions = [] await helper.createDocument() await helper.doAction('codeAction', undefined) }) it('should apply single code action when only is title', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) currActions = [action] await codeActions.doCodeAction(undefined, 'code fix') let lines = await doc.buffer.lines expect(lines).toEqual(['bar']) }) it('should apply single code action when only is QuickFix', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) currActions = [action] await codeActions.doCodeAction(undefined, [CodeActionKind.QuickFix]) let lines = await doc.buffer.lines expect(lines).toEqual(['bar']) }) it('should show disabled code action', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let refactorAction = CodeAction.create('code refactor', edit, CodeActionKind.Refactor) refactorAction.disabled = { reason: 'invalid position' } let fixAction = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) currActions = [refactorAction, fixAction] let p = codeActions.doCodeAction(undefined, undefined, true) let winid = await helper.waitFloat() let win = nvim.createWindow(winid) let buf = await win.buffer let lines = await buf.lines expect(lines.length).toBe(2) expect(lines[1]).toMatch(/code refactor/) await nvim.input('2') await helper.wait(1) await nvim.input('j') await nvim.input('') await helper.waitValue(async () => { let cmdline = await helper.getCmdline() return cmdline.includes('invalid position') }, true) await nvim.input('') await p }) it('should action dialog to choose action', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) currActions = [action, CodeAction.create('foo')] let promise = codeActions.doCodeAction(null, undefined) await helper.waitFloat() let ids = await nvim.call('coc#float#get_float_win_list') as number[] expect(ids.length).toBeGreaterThan(0) await nvim.input('') await promise let lines = await doc.buffer.lines expect(lines).toEqual(['bar']) }) it('should choose code actions by range', async () => { let range: Range disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: ( _document: TextDocument, r: Range, _context: CodeActionContext, _token: CancellationToken ) => { range = r return [CodeAction.create('my title'), CodeAction.create('b'), CodeAction.create('c')] }, }, undefined)) await helper.createDocument() await nvim.setLine('abc') await nvim.command('normal! 0v$') await nvim.input('') await codeActions.doCodeAction('v', 'my title') expect(range).toEqual({ start: { line: 0, character: 0 }, end: { line: 0, character: 3 } }) }) it('should filter by provider kinds', async () => { currActions = [] disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: () => { return [CodeAction.create('my title'), CodeAction.create('b'), CodeAction.create('c')] }, }, undefined, [CodeActionKind.QuickFix])) let doc = await workspace.document let res = await languages.getCodeActions(doc.textDocument, Range.create(0, 0, 1, 1), { only: [CodeActionKind.Refactor], diagnostics: [] }, CancellationToken.None) expect(res).toEqual([]) }) it('should filter by codeAction kind', async () => { currActions = [] disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: () => { return [ CodeAction.create('my title', CodeActionKind.QuickFix), CodeAction.create('b'), Command.create('command', 'command') ] }, resolveCodeAction: () => { return null } }, undefined)) let doc = await workspace.document let res = await languages.getCodeActions(doc.textDocument, Range.create(0, 0, 1, 1), { only: [CodeActionKind.QuickFix], diagnostics: [] }, CancellationToken.None) expect(res.length).toBe(1) let resolved = await languages.resolveCodeAction(res[0], CancellationToken.None) expect(resolved).toBeDefined() await expect(async () => { await codeActions.doCodeAction(null, 'command', true) }).rejects.toThrow(Error) await codeActions.doCodeAction(null, 'cmd', true) let line = await helper.getCmdline() expect(line).toMatch('No cmd code action') }) it('should use quickpick', async () => { helper.updateConfiguration('coc.preferences.floatActions', false) currActions = [CodeAction.create('foo', CodeActionKind.QuickFix), CodeAction.create('bar', CodeActionKind.QuickFix)] let spy = jest.spyOn(window.dialogs, 'requestInputList').mockReturnValue(Promise.resolve(0)) let action let s = jest.spyOn(codeActions, 'applyCodeAction').mockImplementation((a, _token) => { action = a return Promise.resolve() }) await codeActions.doCodeAction(null, undefined) s.mockRestore() spy.mockRestore() expect(action).toBeDefined() expect(action.title).toBe('foo') helper.updateConfiguration('coc.preferences.floatActions', true) }) }) describe('doQuickfix', () => { it('should show message when quickfix action does not exist', async () => { currActions = [] await helper.createDocument() await helper.doAction('doQuickfix') let msg = await helper.getCmdline() expect(msg).toMatch('No quickfix') }) it('should do preferred quickfix action', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) action.isPreferred = true currActions = [CodeAction.create('foo', CodeActionKind.QuickFix), action, CodeAction.create('bar')] await codeActions.doQuickfix() let lines = await doc.buffer.lines expect(lines).toEqual(['bar']) }) }) describe('applyCodeAction', () => { it('should resolve codeAction', async () => { let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', CodeActionKind.QuickFix) action.isPreferred = true currActions = [action] resolvedAction = Object.assign({ edit }, action) let arr = await helper.doAction('quickfixes', 'line') await commands.executeCommand('editor.action.doCodeAction', arr[0]) let lines = await doc.buffer.lines expect(lines).toEqual(['bar']) }) it('should not throw when resolved action is null', async () => { await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let action = CodeAction.create('code fix', CodeActionKind.QuickFix) action.isPreferred = true currActions = [action] resolvedAction = null let arr = await helper.doAction('quickfixes', 'line') await commands.executeCommand('editor.action.doCodeAction', arr[0]) }) it('should throw for disabled action', async () => { let action: any = CodeAction.create('my action', CodeActionKind.Empty) action.disabled = { reason: 'disabled', providerId: 'x' } await expect(async () => { await helper.doAction('doCodeAction', action) }).rejects.toThrow(Error) }) it('should invoke registered command after apply edit', async () => { let called disposables.push(commands.registerCommand('test.execute', async (s: string) => { called = s await nvim.command(s) })) let doc = await helper.createDocument() let edits: TextEdit[] = [] edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) let edit = { changes: { [doc.uri]: edits } } let action = CodeAction.create('code fix', CodeActionKind.QuickFix) action.isPreferred = true currActions = [action] resolvedAction = Object.assign({ edit, command: Command.create('run vim command', 'test.execute', 'normal! $') }, action) let arr = await codeActions.getCurrentCodeActions('line', [CodeActionKind.QuickFix]) await codeActions.applyCodeAction(arr[0]) let lines = await doc.buffer.lines expect(lines).toEqual(['bar']) expect(called).toBe('normal! $') }) }) it('should execute code action with timeout', async () => { disposeAll(disposables) let doc = await helper.createDocument('t.js') let called = false disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: ( _document: TextDocument, _range: Range, _context: CodeActionContext, _token: CancellationToken ) => currActions, resolveCodeAction: ( _action: CodeAction, token: CancellationToken ): ProviderResult => { return new Promise(resolve => { token.onCancellationRequested(() => { called = true resolve(undefined) clearTimeout(timer) }) let timer = setTimeout(() => { resolve(resolvedAction) }, 200) }) } }, undefined)) let action = CodeAction.create('fix all', undefined, CodeActionKind.SourceFixAll) currActions = [action] let res = await codeActions.executeCodeActions(doc, undefined, [CodeActionKind.SourceFixAll], 50) expect(res).toEqual([]) expect(called).toBe(true) }) it('should execute organizeImport code action', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) let action = CodeAction.create('organize import', undefined, CodeActionKind.SourceOrganizeImports) currActions = [action] let edits: TextEdit[] = [] edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar')) let obj = Object.assign({}, action) obj.edit = { changes: { [doc.uri]: edits } } resolvedAction = obj let res = await codeActions.executeCodeActions(doc, undefined, [CodeActionKind.SourceOrganizeImports], 50) expect(res).toEqual([CodeActionKind.SourceOrganizeImports]) let line = doc.getline(0) expect(line).toBe('bar') }) }) ================================================ FILE: src/__tests__/handler/codelens.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CodeLens, Command, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import commands from '../../commands' import events from '../../events' import CodeLensBuffer, { getCommands, getTextAlign } from '../../handler/codelens/buffer' import CodeLensHandler from '../../handler/codelens/index' import languages from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let codeLens: CodeLensHandler let disposables: Disposable[] = [] let srcId: number jest.setTimeout(10000) beforeAll(async () => { await helper.setup() nvim = helper.nvim srcId = await nvim.createNamespace('coc-codelens') codeLens = helper.plugin.getHandler().codeLens }) beforeEach(() => { helper.updateConfiguration('codeLens.enable', true) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) }) async function createBufferWithCodeLens(): Promise { disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { provideCodeLenses: () => { return [{ range: Range.create(0, 0, 0, 1) }] }, resolveCodeLens: codeLens => { codeLens.command = Command.create('save', '__save', 1, 2, 3) return codeLens } })) let doc = await helper.createDocument('e.js') await nvim.call('setline', [1, ['a', 'b', 'c']]) await doc.synchronize() await codeLens.checkProvider() return codeLens.buffers.getItem(doc.bufnr) } describe('codeLenes feature', () => { it('should get text align', async () => { expect(getTextAlign(undefined)).toBe('above') expect(getTextAlign('top')).toBe('above') expect(getTextAlign('eol')).toBe('after') expect(getTextAlign('right_align')).toBe('right') }) it('should not throw when srcId not exists', async () => { let doc = await workspace.document let item = codeLens.buffers.getItem(doc.bufnr) item.clear() await item.doAction(0) }) it('should invoke codeLenes action', async () => { let fn = jest.fn() disposables.push(commands.registerCommand('__save', (...args) => { fn(...args) })) await createBufferWithCodeLens() await helper.doAction('codeLensAction') await nvim.call('cursor', [1, 1]) expect(fn).toHaveBeenCalledWith(1, 2, 3) await nvim.command('normal! G') await helper.doAction('codeLensAction') }) it('should toggle codeLens display', async () => { await codeLens.toggle(999) let line = await helper.getCmdline() expect(line).toMatch('not exists') await createBufferWithCodeLens() await commands.executeCommand('document.toggleCodeLens') let doc = await workspace.document let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true }) expect(res.length).toBe(0) await commands.executeCommand('document.toggleCodeLens') await helper.waitValue(async () => { let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true }) return res.length > 0 }, true) }) it('should return codeLenes when resolve not exists', async () => { let codeLens = CodeLens.create(Range.create(0, 0, 1, 1)) let resolved = await languages.resolveCodeLens(codeLens, CancellationToken.None) expect(resolved).toBeDefined() }) it('should do codeLenes request and resolve codeLenes', async () => { let buf = await createBufferWithCodeLens() let doc = await workspace.document await helper.waitValue(async () => { let codelens = buf.currentCodeLens return Array.isArray(codelens) && codelens[0].command != null }, true) let markers = await doc.buffer.getExtMarks(srcId, 0, -1) expect(markers.length).toBe(1) let codeLenes = buf.currentCodeLens await languages.resolveCodeLens(codeLenes[0], CancellationToken.None) }) it('should refresh on empty changes', async () => { await createBufferWithCodeLens() let doc = await workspace.document await nvim.call('setline', [1, ['a', 'b', 'c']]) await doc.synchronize() let markers = await doc.buffer.getExtMarks(srcId, 0, -1) expect(markers.length).toBeGreaterThan(0) }) it('should work with empty codeLens', async () => { disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { provideCodeLenses: () => { return [] } })) let doc = await helper.createDocument('t.js') let buf = codeLens.buffers.getItem(doc.bufnr) let codelens = buf.currentCodeLens expect(codelens).toBeUndefined() }) it('should change codeLenes position', async () => { helper.updateConfiguration('codeLens.position', 'eol') let bufnr = await nvim.call('bufnr', ['%']) as number let item = codeLens.buffers.getItem(bufnr) expect(item.config.position).toBe('eol') }) it('should refresh codeLens on CursorHold', async () => { disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { provideCodeLenses: document => { let n = document.lineCount let arr: any[] = [] for (let i = 0; i <= n - 2; i++) { arr.push({ range: Range.create(i, 0, i, 1), command: Command.create('save', '__save', i) }) } return arr } })) let doc = await helper.createDocument('example.js') await nvim.call('setline', [1, ['a', 'b', 'c']]) await doc.synchronize() await events.fire('CursorHold', [doc.bufnr]) await helper.waitValue(async () => { let markers = await doc.buffer.getExtMarks(srcId, 0, -1) return markers.length }, 3) helper.updateConfiguration('codeLens.enable', false) await events.fire('CursorHold', [doc.bufnr]) }) it('should cancel codeLenes request on document change', async () => { let cancelled = false disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { provideCodeLenses: (_, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) resolve(null) }) let timer = setTimeout(() => { resolve([{ range: Range.create(0, 0, 0, 1) }, { range: Range.create(1, 0, 1, 1) }]) }, 2000) disposables.push({ dispose: () => { clearTimeout(timer) } }) }) }, resolveCodeLens: codeLens => { codeLens.command = Command.create('save', '__save') return codeLens } })) let doc = await helper.createDocument('codelens.js') await helper.wait(50) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'a\nb\nc')]) expect(cancelled).toBe(true) }) it('should resolve on CursorMoved', async () => { disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { provideCodeLenses: () => { return [{ range: Range.create(190, 0, 190, 1) }, { range: Range.create(191, 0, 191, 1) }] }, resolveCodeLens: async codeLens => { codeLens.command = Command.create('save', '__save') return codeLens } })) let doc = await helper.createDocument('example.js') await nvim.call('cursor', [1, 1]) let arr = new Array(200) arr.fill('') await nvim.call('setline', [1, arr]) await doc.synchronize() await codeLens.checkProvider() await nvim.call('cursor', [190, 1]) await events.fire('CursorMoved', [doc.bufnr, [190, 1], false]) let bufnr = doc.bufnr await helper.waitValue(() => { let buf = codeLens.buffers.getItem(bufnr) return buf && buf.currentCodeLens && buf.currentCodeLens[0].command != null }, true) }, 10000) it('should use picker for multiple codeLenses', async () => { let fn = jest.fn() let resolved = false disposables.push(commands.registerCommand('__save', (...args) => { fn(...args) })) disposables.push(commands.registerCommand('__delete', (...args) => { fn(...args) })) disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { provideCodeLenses: () => { resolved = true return [{ range: Range.create(0, 0, 0, 1), command: Command.create('save', '__save', 1, 2, 3) }, { range: Range.create(0, 1, 0, 2), command: Command.create('save', '__delete', 4, 5, 6) }] } })) let doc = await helper.createDocument('example.js') await nvim.call('setline', [1, ['a', 'b', 'c']]) await doc.synchronize() await codeLens.checkProvider() await helper.waitValue(() => { return resolved }, true) let p = helper.doAction('codeLensAction') await helper.waitPrompt() await nvim.input('') await p expect(fn).toHaveBeenCalledWith(1, 2, 3) }) it('should refresh for failed codeLens request', async () => { let called = 0 let fn = jest.fn() disposables.push(commands.registerCommand('__save', (...args) => { fn(...args) })) disposables.push(commands.registerCommand('__foo', (...args) => { fn(...args) })) disposables.push(languages.registerCodeLensProvider([{ language: '*' }], { provideCodeLenses: () => { called++ if (called == 1) { return null } return [{ range: Range.create(0, 0, 0, 1), command: Command.create('foo', '__foo') }] } })) disposables.push(languages.registerCodeLensProvider([{ language: '*' }], { provideCodeLenses: () => { return [{ range: Range.create(0, 0, 0, 1), command: Command.create('save', '__save') }] } })) let doc = await helper.createDocument('example.js') await helper.wait(50) await nvim.call('setline', [1, ['a', 'b', 'c']]) await codeLens.checkProvider() let markers = await doc.buffer.getExtMarks(srcId, 0, -1) expect(markers.length).toBeGreaterThan(0) let codeLensBuffer = codeLens.buffers.getItem(doc.buffer.id) await codeLensBuffer.forceFetch() let curr = codeLensBuffer.currentCodeLens expect(curr.length).toBeGreaterThan(1) }) it('should use custom separator & position', async () => { helper.updateConfiguration('codeLens.separator', '|') helper.updateConfiguration('codeLens.position', 'eol') let doc = await helper.createDocument('example.js') await nvim.call('setline', [1, ['a', 'b', 'c']]) await doc.synchronize() disposables.push(languages.registerCodeLensProvider([{ language: '*' }], { provideCodeLenses: () => { return [{ range: Range.create(0, 0, 1, 0), command: Command.create('save', '__save') }, { range: Range.create(0, 0, 1, 0), command: Command.create('save', '__save') }] } })) await helper.wait(10) await codeLens.checkProvider() let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true }) expect(res.length).toBe(1) }) it('should get commands from codeLenses', async () => { expect(getCommands(1, undefined)).toEqual([]) let codeLenses = [CodeLens.create(Range.create(0, 0, 0, 0))] expect(getCommands(0, codeLenses)).toEqual([]) codeLenses = [CodeLens.create(Range.create(0, 0, 1, 0)), CodeLens.create(Range.create(2, 0, 3, 0))] codeLenses[0].command = Command.create('save', '__save') expect(getCommands(0, codeLenses).length).toEqual(1) }) }) ================================================ FILE: src/__tests__/handler/commands.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import commandManager from '../../commands' import CommandsHandler from '../../handler/commands' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let commands: CommandsHandler let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim commands = helper.plugin.handler.commands }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('Commands', () => { describe('addVimCommand', () => { it('should register global vim commands', async () => { await commandManager.executeCommand('vim.config') let val = await nvim.getVar('coc_config_init') expect(val).toBe(1) let list = await helper.doAction('commandList') expect(list.includes('vim.config')).toBe(true) }) it('should add vim command with title', async () => { await helper.plugin.cocAction('addCommand', { id: 'bad', cmd: '', title: '' }) commands.addVimCommand({ id: 'list', cmd: 'CocList', title: 'list of coc.nvim' }) let res = commandManager.titles.get('vim.list') expect(res).toBe('list of coc.nvim') commandManager.unregister('vim.list') commandManager.unregister('unknown.command') let list = commands.getCommandList() expect(list.includes('bad')).toBe(false) }) }) describe('commandManager', () => { it('should replace builtin command', async () => { let fn = jest.fn() commandManager.registerCommand('editor.action.restart', () => { fn() }) await commandManager.executeCommand('editor.action.restart') expect(fn).toHaveBeenCalled() }) it('should throw when command not found', async () => { await expect(async () => { await commandManager.executeCommand('') }).rejects.toThrow(Error) }) it('should add to recent', async () => { await commandManager.addRecent('document.checkBuffer', true) let mru = workspace.createMru('commands') let list = await mru.load() expect(list[0]).toBe('document.checkBuffer') }) }) describe('getCommands', () => { it('should get command items', async () => { let res = await helper.doAction('commands') let idx = res.findIndex(o => o.id == 'workspace.showOutput') expect(idx != -1).toBe(true) }) }) describe('repeat', () => { it('should repeat command', async () => { // let buf = await nvim.buffer await nvim.call('setline', [1, ['a', 'b', 'c']]) await nvim.call('cursor', [1, 1]) commands.addVimCommand({ id: 'remove', cmd: 'normal! dd' }) await helper.doAction('runCommand', 'vim.remove') await helper.waitFor('getline', ['.'], 'b') await helper.doAction('repeatCommand') await helper.waitFor('getline', ['.'], 'c') }) }) describe('runCommand', () => { it('should open command list without id', async () => { await commands.runCommand() await helper.waitFor('bufname', ['%'], 'list:///commands') }) }) }) ================================================ FILE: src/__tests__/handler/documentColors.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Color, ColorInformation, ColorPresentation, Disposable, Position, Range } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import commands from '../../commands' import { toHexString } from '../../util/color' import Colors from '../../handler/colors/index' import languages from '../../languages' import { ProviderResult } from '../../provider' import { disposeAll } from '../../util' import path from 'path' import helper from '../helper' import workspace from '../../workspace' import events from '../../events' let nvim: Neovim let state = 'normal' let colors: Colors let disposables: Disposable[] = [] let colorPresentations: ColorPresentation[] = [] let disposable: Disposable beforeAll(async () => { await helper.setup() nvim = helper.nvim await nvim.command(`source ${path.join(process.cwd(), 'autoload/coc/color.vim')}`) colors = helper.plugin.getHandler().colors disposable = languages.registerDocumentColorProvider([{ language: '*' }], { provideColorPresentations: ( _color: Color, _context: { document: TextDocument; range: Range }, _token: CancellationToken ): ColorPresentation[] => colorPresentations, provideDocumentColors: ( document: TextDocument, _token: CancellationToken ): ProviderResult => { if (state == 'empty') return [] if (state == 'error') return Promise.reject(new Error('no color')) let matches = Array.from((document.getText() as any).matchAll(/#\w{6}/g)) as any return matches.map(o => { let start = document.positionAt(o.index) let end = document.positionAt(o.index + o[0].length) return { range: Range.create(start, end), color: getColor(255, 255, 255) } }) } }) }) beforeEach(() => { helper.updateConfiguration('colors.filetypes', ['*']) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { state = 'normal' colorPresentations = [] disposeAll(disposables) await helper.reset() }) function getColor(r: number, g: number, b: number): Color { return { red: r / 255, green: g / 255, blue: b / 255, alpha: 1 } } describe('Colors', () => { describe('utils', () => { it('should get hex string', () => { let color = getColor(255, 255, 255) let hex = toHexString(color) expect(hex).toBe('ffffff') }) }) describe('configuration', () => { it('should toggle enable state on configuration change', async () => { let doc = await helper.createDocument() helper.updateConfiguration('colors.filetypes', []) let enabled = colors.isEnabled(doc.bufnr) expect(enabled).toBe(false) helper.updateConfiguration('colors.enable', true) enabled = colors.isEnabled(doc.bufnr) expect(enabled).toBe(true) helper.updateConfiguration('colors.enable', false) enabled = colors.isEnabled(doc.bufnr) expect(enabled).toBe(false) }) }) describe('commands', () => { it('should register editor.action.pickColor command', async () => { await helper.mockFunction('coc#color#pick_color', [0, 0, 0]) let doc = await helper.createDocument() await nvim.setLine('#ffffff') doc.forceSync() await colors.doHighlight(doc.bufnr) await commands.executeCommand('editor.action.pickColor') let line = await nvim.getLine() expect(line).toBe('#000000') }) it('should register editor.action.colorPresentation command', async () => { colorPresentations = [ColorPresentation.create('red'), ColorPresentation.create('#ff0000')] let doc = await helper.createDocument() await nvim.setLine('#ffffff') await doc.synchronize() await colors.doHighlight(doc.bufnr) let p = commands.executeCommand('editor.action.colorPresentation') await helper.waitPrompt() await nvim.input('1') await p let line = await nvim.getLine() expect(line).toBe('red') }) it('should register document.toggleColors command', async () => { helper.updateConfiguration('colors.filetypes', []) helper.updateConfiguration('colors.enable', true) let doc = await workspace.document await events.fire('BufUnload', [doc.bufnr]) await expect(async () => { await commands.executeCommand('document.toggleColors') }).rejects.toThrow(Error) doc = await helper.createDocument() expect(colors.isEnabled(doc.bufnr)).toBe(true) await commands.executeCommand('document.toggleColors') let enabled = colors.isEnabled(doc.bufnr) expect(enabled).toBe(false) await commands.executeCommand('document.toggleColors') enabled = colors.isEnabled(doc.bufnr) expect(enabled).toBe(true) }) }) describe('doHighlight', () => { it('should merge colors of providers', async () => { disposables.push(languages.registerDocumentColorProvider([{ language: '*' }], { provideColorPresentations: (): ColorPresentation[] => colorPresentations, provideDocumentColors: ( ): ProviderResult => { return [{ range: Range.create(0, 0, 1, 0), color: getColor(0, 0, 0) }, { range: Range.create(0, 0, 0, 7), color: getColor(1, 1, 1) }] } })) disposables.push(languages.registerDocumentColorProvider([{ language: '*' }], { provideColorPresentations: (): ColorPresentation[] => colorPresentations, provideDocumentColors: ( ): ProviderResult => { return null } })) let doc = await workspace.document await nvim.setLine('#ffffff #ff0000') await doc.synchronize() let colors = await languages.provideDocumentColors(doc.textDocument, CancellationToken.None) expect(colors.length).toBe(3) let color = ColorInformation.create(Range.create(0, 0, 1, 0), getColor(0, 0, 0)) let presentation = await languages.provideColorPresentations(color, doc.textDocument, CancellationToken.None) expect(presentation).toEqual([]) }) it('should clearHighlight on empty result', async () => { let doc = await helper.createDocument() await nvim.setLine('#ffffff') state = 'empty' await colors.doHighlight(doc.bufnr) let res = colors.hasColor(doc.bufnr) expect(res).toBe(false) }) it('should highlight after ColorScheme event', async () => { let doc = await helper.createDocument() await nvim.setLine('#ffffff #ff0000') await doc.synchronize() await colors.doHighlight(doc.bufnr) await events.fire('ColorScheme', []) expect(colors.hasColor(doc.bufnr)).toBe(true) }) it('should not throw on error result', async () => { let doc = await helper.createDocument() await nvim.setLine('#ffffff') state = 'error' let err try { await colors.doHighlight(doc.bufnr) } catch (e) { err = e } expect(err).toBeUndefined() }) it('should highlight after document changed', async () => { let doc = await helper.createDocument() await colors.doHighlight(doc.bufnr) expect(colors.hasColor(doc.bufnr)).toBe(false) expect(colors.hasColorAtPosition(doc.bufnr, Position.create(0, 1))).toBe(false) await nvim.setLine('#ffffff #ff0000') await doc.synchronize() await helper.waitValue(() => { return colors.hasColorAtPosition(doc.bufnr, Position.create(0, 1)) }, true) expect(colors.hasColor(doc.bufnr)).toBe(true) }) it('should clearHighlight on clearHighlight', async () => { let doc = await helper.createDocument() await nvim.setLine('#ffffff #ff0000') await doc.synchronize() await colors.doHighlight(doc.bufnr) expect(colors.hasColor(doc.bufnr)).toBe(true) colors.clearHighlight(doc.bufnr) expect(colors.hasColor(doc.bufnr)).toBe(false) }) it('should highlight colors', async () => { let doc = await helper.createDocument() await nvim.setLine('#ffffff') await colors.doHighlight(doc.bufnr) let exists = await nvim.call('hlexists', 'BGffffff') expect(exists).toBe(1) }) }) describe('hasColor()', () => { it('should return false when bufnr does not exist', async () => { let res = colors.hasColor(99) colors.clearHighlight(99) expect(res).toBe(false) }) }) describe('getColorInformation()', () => { it('should return null when highlighter does not exist', async () => { let res = await colors.getColorInformation(99) expect(res).toBe(null) }) it('should return null when color not found', async () => { let doc = await helper.createDocument() await nvim.setLine('#ffffff foo ') doc.forceSync() await colors.doHighlight(doc.bufnr) await nvim.call('cursor', [1, 12]) let res = await colors.getColorInformation(doc.bufnr) expect(res).toBe(null) }) }) describe('hasColorAtPosition()', () => { it('should return false when bufnr does not exist', async () => { let res = colors.hasColorAtPosition(99, Position.create(0, 0)) expect(res).toBe(false) }) }) describe('pickPresentation()', () => { it('should show warning when color does not exist', async () => { await helper.createDocument() await colors.pickPresentation() let msg = await helper.getCmdline() expect(msg).toMatch('Color not found') }) it('should not throw when presentations do not exist', async () => { colorPresentations = [] let doc = await helper.createDocument() await nvim.setLine('#ffffff') doc.forceSync() await colors.doHighlight(99) await colors.doHighlight(doc.bufnr) await helper.doAction('colorPresentation') }) it('should pick presentations', async () => { colorPresentations = [ColorPresentation.create('red'), ColorPresentation.create('#ff0000')] let doc = await helper.createDocument() await nvim.setLine('#ffffff') doc.forceSync() await colors.doHighlight(doc.bufnr) let p = helper.doAction('colorPresentation') await helper.waitPrompt() await nvim.input('1') await p let line = await nvim.getLine() expect(line).toBe('red') }) }) describe('pickColor()', () => { it('should show warning when color does not exist', async () => { await helper.createDocument() await colors.pickColor() let msg = await helper.getCmdline() expect(msg).toMatch('not found') }) it('should pickColor', async () => { await helper.mockFunction('coc#color#pick_color', [0, 0, 0]) let doc = await helper.createDocument() await nvim.setLine('#ffffff') doc.forceSync() await colors.doHighlight(doc.bufnr) await helper.doAction('pickColor') let line = await nvim.getLine() expect(line).toBe('#000000') }) it('should not throw when pick color return 0', async () => { await helper.mockFunction('coc#color#pick_color', 0) let doc = await helper.createDocument() await nvim.setLine('#ffffff') doc.forceSync() await colors.doHighlight(doc.bufnr) await helper.doAction('pickColor') let line = await nvim.getLine() expect(line).toBe('#ffffff') }) it('should return null when provider not exists', async () => { disposable.dispose() let doc = await workspace.document let color = ColorInformation.create(Range.create(0, 0, 0, 6), Color.create(100, 100, 100, 0)) let res = await languages.provideColorPresentations(color, doc.textDocument, CancellationToken.None) expect(res).toBeNull() }) }) }) ================================================ FILE: src/__tests__/handler/documentLinks.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, DocumentLink, Range } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import events from '../../events' import LinksHandler, { sameLinks } from '../../handler/links' import languages from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let links: LinksHandler let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim links = helper.plugin.getHandler().links }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('Links', () => { it('should check sameLinks', () => { expect(sameLinks([], [])).toBe(true) expect(sameLinks([{ range: Range.create(0, 0, 0, 1) }], [])).toBe(false) expect(sameLinks([{ range: Range.create(0, 0, 0, 1) }], [{ range: Range.create(0, 0, 1, 0) }])).toBe(false) }) it('should get document links', async () => { disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks: (_doc, _token) => { return [ DocumentLink.create(Range.create(0, 0, 0, 5), 'test:///foo'), DocumentLink.create(Range.create(1, 0, 1, 5), 'test:///bar') ] } })) let res = await helper.doAction('links') expect(res.length).toBe(2) }) it('should merge link results', async () => { disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks: () => { return [ DocumentLink.create(Range.create(0, 0, 0, 5), 'test:///foo'), DocumentLink.create(Range.create(1, 0, 1, 5), 'test:///bar') ] } })) disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks: () => { return [ DocumentLink.create(Range.create(1, 0, 1, 5), 'test:///bar'), DocumentLink.create(Range.create(2, 0, 2, 5), 'test:///x'), ] } })) disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks: () => { return null } })) let res = await links.getLinks() expect(res.length).toBe(3) let link = await languages.resolveDocumentLink(res[0], CancellationToken.None) expect(link).toBeDefined() }) it('should throw error when link target not resolved', async () => { disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks(_doc, _token) { return [ DocumentLink.create(Range.create(0, 0, 0, 5)) ] }, resolveDocumentLink(link) { return link } })) let res = await links.getLinks() let err try { await links.openLink(res[0]) } catch (e) { err = e } expect(err).toBeDefined() }) it('should return link when resolve undefined', async () => { disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks(_doc, _token) { return [DocumentLink.create(Range.create(0, 0, 0, 5), 'foo://1')] }, resolveDocumentLink() { return undefined } })) let res = await links.getLinks() let link = await languages.resolveDocumentLink(res[0], CancellationToken.None) expect(link).toBeDefined() }) it('should cancel resolve on InsertEnter', async () => { helper.updateConfiguration('links.tooltip', true) let doc = await workspace.document let called = false let cancelled = false disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks(_doc, _token) { return [DocumentLink.create(Range.create(0, 0, 0, 5))] }, resolveDocumentLink(link, token) { called = true return new Promise(resolve => { token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) resolve(undefined) }) let timer = setTimeout(() => { resolve(link) }, 500) }) } })) let p = links.showTooltip() await helper.waitValue(() => { return called }, true) await events.fire('InsertEnter', [doc.bufnr]) await p expect(cancelled).toBe(true) }) it('should open link at current position', async () => { await nvim.setLine('foo') await nvim.command('normal! 0') disposables.push(workspace.registerTextDocumentContentProvider('test', { provideTextDocumentContent: () => { return 'test' } })) disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks(_doc, _token) { return [ DocumentLink.create(Range.create(0, 0, 0, 5)), ] }, resolveDocumentLink(link) { link.target = 'test:///foo' return link } })) await helper.doAction('openLink') let bufname = await nvim.call('bufname', '%') expect(bufname).toBe('test:///foo') await nvim.call('setline', [1, ['a', 'b', 'c']]) await nvim.call('cursor', [3, 1]) let res = await links.openCurrentLink() expect(res).toBe(false) }) it('should return false when current links not found', async () => { await nvim.setLine('foo') await nvim.command('normal! 0') disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks(_doc, _token) { return [] } })) let res = await links.openCurrentLink() expect(res).toBe(false) }) it('should show tooltip', async () => { await nvim.setLine('foo') await nvim.call('cursor', [1, 1]) let resolve = false disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks(_doc, _token) { let link = DocumentLink.create(Range.create(0, 0, 0, 5)) link.tooltip = 'test' return [link] }, resolveDocumentLink(link) { if (!resolve) return link.target = 'http://example.com' return link } })) await links.showTooltip() let win = await helper.getFloat() expect(win).toBeUndefined() helper.updateConfiguration('links.tooltip', true) await links.showTooltip() win = await helper.getFloat() expect(win).toBeUndefined() resolve = true await links.showTooltip() win = await helper.getFloat() let buf = await win.buffer let lines = await buf.lines expect(lines[0]).toMatch('test') }) it('should enable tooltip on CursorHold', async () => { let doc = await workspace.document helper.updateConfiguration('links.tooltip', true) await nvim.setLine('http://www.baidu.com') await nvim.call('cursor', [1, 1]) let link = await links.getCurrentLink() expect(link).toBeDefined() await events.fire('CursorHold', [doc.bufnr]) let win = await helper.getFloat() let buf = await win.buffer let lines = await buf.lines expect(lines[0]).toMatch('baidu') }) }) describe('LinkBuffer', () => { it('should getLinks', async () => { let doc = await workspace.document let buf = links.getBuffer(doc.bufnr) await buf.getLinks() expect(buf.links).toEqual([]) let timeout = 100 disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks: (_doc, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) resolve(undefined) }) let timer = setTimeout(() => { resolve([ DocumentLink.create(Range.create(0, 0, 0, 5), 'test:///foo'), DocumentLink.create(Range.create(1, 0, 1, 5), 'test:///bar') ]) }, timeout) }) } })) let p = buf.getLinks() p = buf.getLinks() buf.cancel() await p expect(buf.links).toEqual([]) }) it('should do highlight', async () => { disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], { provideDocumentLinks: (doc: TextDocument) => { let links: DocumentLink[] = [] for (let i = 0; i < doc.lineCount - 1; i++) { links.push(DocumentLink.create(Range.create(i, 0, i, 1), 'test:///bar')) } return links } })) helper.updateConfiguration('links.highlight', true) let doc = await helper.createDocument() await nvim.setLine('foo') await doc.synchronize() let buf = links.getBuffer(doc.bufnr) await helper.waitValue(() => { return buf.links?.length }, 1) await nvim.call('append', [0, ['foo']]) doc._forceSync() await helper.waitValue(() => { return buf.links?.length }, 2) await nvim.setLine('foo') doc._forceSync() let hls = await buf.buffer.getHighlights('links') expect(hls.length).toBe(2) }) }) ================================================ FILE: src/__tests__/handler/fold.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CancellationTokenSource, Disposable, FoldingRange } from 'vscode-languageserver-protocol' import FoldHandler from '../../handler/fold' import languages from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let folds: FoldHandler let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim folds = helper.plugin.getHandler().fold }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('Folds', () => { it('should return empty array when provider does not exist', async () => { let doc = await workspace.document let token = (new CancellationTokenSource()).token expect(await languages.provideFoldingRanges(doc.textDocument, {}, token)).toEqual([]) }) it('should return false when no fold ranges found', async () => { disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges(_doc) { return [] } })) await helper.wait(50) let res = await helper.doAction('fold') expect(res).toBe(false) }) it('should fold all fold ranges', async () => { disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges(_doc) { return [FoldingRange.create(1, 3), FoldingRange.create(4, 6, 0, 0, 'comment')] } })) await helper.wait(50) await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']]) let res = await folds.fold() expect(res).toBe(true) let closed = await nvim.call('foldclosed', [2]) expect(closed).toBe(2) closed = await nvim.call('foldclosed', [5]) expect(closed).toBe(5) }) it('should merge folds from all providers', async () => { let doc = await workspace.document disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges() { return [FoldingRange.create(2, 3), FoldingRange.create(4, 6)] } })) disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges() { return [FoldingRange.create(1, 2), FoldingRange.create(5, 6), FoldingRange.create(7, 8)] } })) await helper.wait(50) await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']]) await doc.synchronize() let foldingRanges = await languages.provideFoldingRanges(doc.textDocument, {}, CancellationToken.None) expect(foldingRanges.length).toBe(4) }) it('should ignore range start at the same line', async () => { let doc = await workspace.document disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges() { return [FoldingRange.create(2, 3), FoldingRange.create(4, 6)] } })) disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges() { return [FoldingRange.create(4, 5)] } })) await helper.wait(50) await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']]) await doc.synchronize() let foldingRanges = await languages.provideFoldingRanges(doc.textDocument, {}, CancellationToken.None) expect(foldingRanges.length).toBe(2) }) it('should fold comment ranges', async () => { disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { provideFoldingRanges(_doc) { return [FoldingRange.create(1, 3), FoldingRange.create(4, 6, 0, 0, 'comment')] } })) await helper.wait(50) await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']]) let res = await folds.fold('comment') expect(res).toBe(true) let closed = await nvim.call('foldclosed', [2]) expect(closed).toBe(-1) closed = await nvim.call('foldclosed', [5]) expect(closed).toBe(5) }) }) ================================================ FILE: src/__tests__/handler/format.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CancellationTokenSource, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import commands from '../../commands' import Format from '../../handler/format' import languages, { ProviderName } from '../../languages' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let format: Format beforeAll(async () => { await helper.setup() nvim = helper.nvim format = helper.plugin.getHandler().format }) beforeEach(() => { helper.updateConfiguration('coc.preferences.formatOnType', true) }) afterEach(async () => { await helper.reset() disposeAll(disposables) }) afterAll(async () => { await helper.shutdown() }) describe('format handler', () => { describe('documentFormat', () => { it('should return null when format provider not exists', async () => { let doc = await workspace.document let res = await languages.provideDocumentFormattingEdits(doc.textDocument, { insertSpaces: false, tabSize: 2 }, CancellationToken.None) expect(res).toBeNull() }) it('should throw when provider not found', async () => { await expect(async () => { await commands.executeCommand('editor.action.formatDocument', 999) }).rejects.toThrow(Error) await expect(async () => { await commands.executeCommand('editor.action.formatDocument') }).rejects.toThrow(Error) await expect(async () => { let doc = await workspace.document await commands.executeCommand('editor.action.formatDocument', doc.uri) }).rejects.toThrow(Error) }) it('should return false when get empty edits ', async () => { disposables.push(languages.registerDocumentFormatProvider(['*'], { provideDocumentFormattingEdits: () => { return [] } })) let doc = await helper.createDocument() let res = await format.documentFormat(doc) expect(res).toBe(false) }) it('should use provider that have higher score', async () => { disposables.push(languages.registerDocumentFormatProvider([{ language: 'vim' }], { provideDocumentFormattingEdits: () => { return [TextEdit.insert(Position.create(0, 0), ' ')] } })) disposables.push(languages.registerDocumentFormatProvider(['*'], { provideDocumentFormattingEdits: () => { return [] } })) let doc = await helper.createDocument('t.vim') let res = await languages.provideDocumentFormattingEdits(doc.textDocument, { tabSize: 2, insertSpaces: false }, CancellationToken.None) expect(res.length).toBe(1) }) it('should not fallback to range formatter when document formatter returns null', async () => { let called = false disposables.push(languages.registerDocumentFormatProvider([{ language: 'text' }], { provideDocumentFormattingEdits: () => { return null } })) disposables.push(languages.registerDocumentRangeFormatProvider([{ language: 'text' }], { provideDocumentRangeFormattingEdits: () => { called = true return [TextEdit.insert(Position.create(0, 0), ' ')] } })) let doc = await helper.createDocument('t.txt') let edits = await languages.provideDocumentFormattingEdits(doc.textDocument, { tabSize: 2, insertSpaces: false }, CancellationToken.None) expect(edits).toBeNull() expect(called).toBe(false) }) it('should fallback to range formatter when document formatter not exists', async () => { let called = false disposables.push(languages.registerDocumentRangeFormatProvider([{ language: 'text' }], { provideDocumentRangeFormattingEdits: () => { called = true return [TextEdit.insert(Position.create(0, 0), ' ')] } })) let doc = await helper.createDocument('t.txt') let edits = await languages.provideDocumentFormattingEdits(doc.textDocument, { tabSize: 2, insertSpaces: false }, CancellationToken.None) expect(called).toBe(true) expect(edits.length).toBe(1) }) it('should format current buffer', async () => { disposables.push(languages.registerDocumentFormatProvider([{ language: 'vim' }], { provideDocumentFormattingEdits: () => { return [TextEdit.insert(Position.create(0, 0), ' ')] } })) await helper.createDocument('t.vim') await commands.executeCommand('editor.action.format') let line = await nvim.line expect(line).toBe(' ') }) it('should use specified format provider', async () => { helper.updateConfiguration('coc.preferences.formatterExtension', 'foo', disposables) disposables.push(languages.registerDocumentFormatProvider([{ language: '*' }], { provideDocumentFormattingEdits: () => { return [TextEdit.insert(Position.create(0, 0), ' ')] } })) let doc = await workspace.document let res = await format.documentFormat(doc) expect(res).toBe(true) let provider = { provideDocumentFormattingEdits: doc => { let line = doc.lines[0] as string return [TextEdit.replace(Range.create(0, 0, 0, line.length), 'foo')] } } provider['__extensionName'] = 'foo' disposables.push(languages.registerDocumentFormatProvider([{ language: '*' }], provider)) await format.documentFormat(doc) let line = doc.getline(0) expect(line).toBe('foo') }) }) describe('rangeFormat', () => { it('should return null when provider does not exist', async () => { let doc = (await workspace.document).textDocument let range = Range.create(0, 0, 1, 0) let options = await workspace.getFormatOptions() let token = (new CancellationTokenSource()).token expect(await languages.provideDocumentRangeFormattingEdits(doc, range, options, token)).toBe(null) expect(languages.hasProvider(ProviderName.FormatOnType, doc)).toBe(false) expect(languages.hasProvider(ProviderName.OnTypeEdit, doc)).toBe(false) let edits = await languages.provideDocumentFormattingEdits(doc, options, token) expect(edits).toBe(null) }) it('should return -1 when range not exists', async () => { disposables.push(languages.registerDocumentRangeFormatProvider(['*'], { provideDocumentRangeFormattingEdits: () => { return [] } }, 1)) let spy = jest.spyOn(window, 'getSelectedRange').mockImplementation(() => { return Promise.resolve(null) }) let doc = await workspace.document let res = await format.documentRangeFormat(doc, 'v') spy.mockRestore() expect(res).toBe(-1) }) it('should invoke range format', async () => { disposables.push(languages.registerDocumentRangeFormatProvider(['text'], { provideDocumentRangeFormattingEdits: (_document, range) => { let lines: number[] = [] for (let i = range.start.line; i <= range.end.line; i++) { lines.push(i) } return lines.map(i => { return TextEdit.insert(Position.create(i, 0), ' ') }) } }, 1)) let doc = await helper.createDocument() doc.setFiletype('text') await nvim.call('setline', [1, ['a', 'b', 'c']]) await nvim.command('normal! ggvG') await nvim.input('') expect(languages.hasFormatProvider(doc.textDocument)).toBe(true) expect(languages.hasProvider(ProviderName.Format, doc.textDocument)).toBe(true) await helper.doAction('formatSelected', 'v') let buf = nvim.createBuffer(doc.bufnr) let lines = await buf.lines expect(lines).toEqual([' a', ' b', ' c']) let options = await workspace.getFormatOptions(doc.bufnr) let token = (new CancellationTokenSource()).token let edits = await languages.provideDocumentFormattingEdits(doc.textDocument, options, token) expect(edits.length).toBeGreaterThan(0) }) it('should format range by formatexpr option', async () => { let range: Range disposables.push(languages.registerDocumentRangeFormatProvider(['text'], { provideDocumentRangeFormattingEdits: (_document, r) => { range = r return [] } })) let doc = await helper.createDocument() doc.setFiletype('text') await nvim.call('setline', [1, ['a', 'b', 'c']]) await nvim.command(`setl formatexpr=CocAction('formatSelected')`) await nvim.command('normal! ggvGgq') expect(range).toEqual({ start: { line: 0, character: 0 }, end: { line: 3, character: 0 } }) }) }) describe('formatOnType', () => { it('should invoke format', async () => { disposables.push(languages.registerDocumentFormatProvider(['text'], { provideDocumentFormattingEdits: () => { return [TextEdit.insert(Position.create(0, 0), ' ')] } })) let doc = await helper.createDocument() doc.setFiletype('text') await nvim.setLine('foo') await helper.doAction('format') let line = await nvim.line expect(line).toEqual(' foo') }) it('should respect formatOnTypeFiletypes', async () => { helper.updateConfiguration('coc.preferences.formatOnTypeFiletypes', ['*']) expect(format.shouldFormatOnType('vim')).toBe(true) helper.updateConfiguration('coc.preferences.formatOnTypeFiletypes', ['txt']) let doc = await helper.createDocument('t.vim') let res = await format.tryFormatOnType('\n', doc) expect(res).toBe(false) }) it('should not format on type when disabled by variable', async () => { disposables.push(languages.registerDocumentFormatProvider(['*'], { provideDocumentFormattingEdits: () => { return [TextEdit.insert(Position.create(0, 0), ' ')] } })) nvim.pauseNotification() nvim.command('e foo', true) nvim.command('let b:coc_disable_autoformat = 1', true) await nvim.resumeNotification() let doc = await workspace.document let res = await format.tryFormatOnType('\n', doc) expect(res).toBe(false) }) it('should does format on type', async () => { let doc = await workspace.document disposables.push(languages.registerOnTypeFormattingEditProvider(['*'], { provideOnTypeFormattingEdits: () => { return [TextEdit.insert(Position.create(0, 0), ' ')] } }, ['|'])) let res = await format.tryFormatOnType(';', doc) expect(res).toBe(false) await helper.edit() await nvim.input('i|') await helper.waitFor('getline', ['.'], ' |') let cursor = await window.getCursorPosition() expect(cursor).toEqual({ line: 0, character: 3 }) }) it('should return null when provider not found', async () => { let doc = await workspace.document let res = await languages.provideDocumentOnTypeEdits('|', doc.textDocument, Position.create(0, 0), CancellationToken.None) expect(res).toBeNull() }) it('should adjust cursor after format on type', async () => { disposables.push(languages.registerOnTypeFormattingEditProvider(['text'], { provideOnTypeFormattingEdits: () => { return [ TextEdit.insert(Position.create(0, 0), ' '), TextEdit.insert(Position.create(0, 2), 'end') ] } }, ['|'])) disposables.push(languages.registerOnTypeFormattingEditProvider([{ language: '*' }], { provideOnTypeFormattingEdits: () => { return [] } })) let doc = await helper.createDocument() doc.setFiletype('text') await nvim.setLine('"') await nvim.input('i|') await helper.waitFor('getline', ['.'], ' |"end') let cursor = await window.getCursorPosition() expect(cursor).toEqual({ line: 0, character: 3 }) }) }) describe('bracketEnterImprove', () => { afterEach(() => { nvim.command('iunmap ', true) }) it('should not throw for buffer not attached', async () => { await nvim.command(`edit +setl\\ buftype=nofile foo`) let doc = await workspace.document expect(doc.attached).toBe(false) await format.handleEnter(doc.bufnr) }) it('should format vim file on enter', async () => { let buf = await helper.edit('foo.vim') await buf.setOption('expandtab', true) await nvim.command(`inoremap pumvisible() ? coc#_select_confirm() : "\\u\\\\=coc#on_enter()\\"`) await nvim.setLine('let foo={}') await nvim.command(`normal! gg$`) await nvim.input('i') await nvim.eval(`feedkeys("\\", 'im')`) await helper.waitFor('getline', [2], ' \\ ') let lines = await buf.lines expect(lines).toEqual(['let foo={', ' \\ ', ' \\ }']) }) it('should use tab on format', async () => { let buf = await helper.edit('foo.vim') await buf.setOption('expandtab', false) await nvim.command(`inoremap pumvisible() ? coc#_select_confirm() : "\\u\\\\=coc#on_enter()\\"`) await nvim.setLine('let foo={}') await nvim.command(`normal! gg$`) await nvim.input('i') await nvim.eval(`feedkeys("\\", 'im')`) await helper.waitFor('getline', ['.'], '\t\\ ') }) it('should add new line between bracket', async () => { let buf = await helper.edit() await nvim.command(`inoremap pumvisible() ? coc#_select_confirm() : "\\u\\\\=coc#on_enter()\\"`) await nvim.setLine(' {}') await nvim.command(`normal! gg$`) await nvim.input('i') await nvim.eval(`feedkeys("\\", 'im')`) await helper.waitFor('getline', [2], ' ') let lines = await buf.lines expect(lines).toEqual([' {', ' ', ' }']) }) }) describe('logProvider()', () => { it('should log provider', () => { format.logProvider(1, []) format.logProvider(1, null) let edits = [TextEdit.insert(Position.create(1, 1), 'foo')] format.logProvider(1, edits) let called = false Object.defineProperty(edits, '__extensionName', { get: () => { called = true return 'name' } }) format.logProvider(1, edits) expect(called).toBe(true) }) }) }) ================================================ FILE: src/__tests__/handler/highlights.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable, DocumentHighlightKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import commands from '../../commands' import Highlights from '../../handler/highlights' import languages from '../../languages' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let highlights: Highlights beforeAll(async () => { await helper.setup() nvim = helper.nvim highlights = helper.plugin.handler.documentHighlighter }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) disposables = [] }) function registerProvider(): void { disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: async document => { let word = await nvim.eval('expand("")') // let word = document.get let matches = Array.from((document.getText() as any).matchAll(/\w+/g)) as any[] let filtered = matches.filter(o => o[0] == word) return filtered.map((o, i) => { let start = document.positionAt(o.index) let end = document.positionAt(o.index + o[0].length) return { range: Range.create(start, end), kind: i == 0 ? DocumentHighlightKind.Text : i % 2 == 0 ? DocumentHighlightKind.Read : DocumentHighlightKind.Write } }).concat([{ range: undefined, kind: 2 }]) } })) } describe('document highlights', () => { function registerTimerProvider(fn: Function, timeout: number): void { disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: (_document, _position, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) fn() resolve([]) }) let timer = setTimeout(() => { resolve([{ range: Range.create(0, 0, 0, 3) }]) }, timeout) }) } })) } it('should not throw when no range to jump', async () => { let fn = jest.fn() registerTimerProvider(fn, 10) await commands.executeCommand('document.jumpToNextSymbol') await commands.executeCommand('document.jumpToPrevSymbol') }) it('should jump to previous range', async () => { disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: () => { return [{ range: Range.create(0, 0, 0, 1), kind: DocumentHighlightKind.Read }, { range: Range.create(0, 2, 0, 3), kind: DocumentHighlightKind.Read }] } })) await nvim.setLine('foo bar') await nvim.command('normal! $') await commands.executeCommand('document.jumpToPrevSymbol') let cur = await window.getCursorPosition() expect(cur).toEqual(Position.create(0, 2)) await commands.executeCommand('document.jumpToPrevSymbol') cur = await window.getCursorPosition() expect(cur).toEqual(Position.create(0, 0)) await commands.executeCommand('document.jumpToPrevSymbol') cur = await window.getCursorPosition() expect(cur).toEqual(Position.create(0, 2)) }) it('should jump to next range', async () => { disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: () => { return [{ range: Range.create(0, 0, 0, 1), kind: DocumentHighlightKind.Read }, { range: Range.create(0, 2, 0, 3), kind: DocumentHighlightKind.Read }] } })) await nvim.setLine('foo bar') await nvim.command('normal! ^') await commands.executeCommand('document.jumpToNextSymbol') let cur = await window.getCursorPosition() expect(cur).toEqual(Position.create(0, 2)) await commands.executeCommand('document.jumpToNextSymbol') cur = await window.getCursorPosition() expect(cur).toEqual(Position.create(0, 0)) await commands.executeCommand('document.jumpToNextSymbol') cur = await window.getCursorPosition() expect(cur).toEqual(Position.create(0, 2)) }) it('should not throw when provide throws', async () => { disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: () => { return null } })) disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: () => { throw new Error('fake error') } })) disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: () => { return [{ range: Range.create(0, 0, 0, 3), kind: DocumentHighlightKind.Read }] } })) let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) let res = await highlights.getHighlights(doc, Position.create(0, 0)) expect(res).toBeDefined() }) it('should return null when highlights provide not exist', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) let res = await highlights.getHighlights(doc, Position.create(0, 0)) expect(res).toBeNull() }) it('should cancel request on CursorMoved', async () => { let fn = jest.fn() registerTimerProvider(fn, 3000) await helper.edit() await nvim.setLine('foo') let p = highlights.highlight() await helper.wait(50) await nvim.call('cursor', [1, 2]) await p expect(fn).toHaveBeenCalled() }) it('should cancel on timeout', async () => { helper.updateConfiguration('documentHighlight.timeout', 10) let fn = jest.fn() registerTimerProvider(fn, 3000) await helper.edit() await nvim.setLine('foo') await highlights.highlight() expect(fn).toHaveBeenCalled() }) it('should add highlights to symbols', async () => { registerProvider() await helper.createDocument() await nvim.setLine('foo bar foo foo bar') await helper.doAction('highlight') let winid = await nvim.call('win_getid') as number expect(highlights.hasHighlights(winid)).toBe(true) }) it('should return highlight ranges', async () => { registerProvider() await helper.createDocument() await nvim.setLine('foo bar foo') let res = await helper.doAction('symbolRanges') expect(res.length).toBe(2) }) it('should return null when cursor not in word range', async () => { disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { provideDocumentHighlights: () => { return [{ range: Range.create(0, 0, 0, 3) }] } })) let doc = await helper.createDocument() await nvim.setLine(' oo') await nvim.call('cursor', [1, 2]) let res = await highlights.getHighlights(doc, Position.create(0, 0)) expect(res).toBeNull() }) it('should not throw when document is command line', async () => { await nvim.call('feedkeys', ['q:', 'in']) let doc = await workspace.document expect(doc.isCommandLine).toBe(true) await highlights.highlight() await nvim.input('') }) it('should not throw when provider not found', async () => { disposeAll(disposables) await helper.createDocument() await nvim.setLine(' oo') await nvim.call('cursor', [1, 2]) await highlights.highlight() }) }) ================================================ FILE: src/__tests__/handler/hover.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, Hover, MarkedString, MarkupKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import HoverHandler, { addDefinitions, addDocument, isDocumentation, readLines } from '../../handler/hover' import languages from '../../languages' import { Documentation } from '../../types' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let hover: HoverHandler let disposables: Disposable[] = [] let hoverResult: Hover beforeAll(async () => { await helper.setup() nvim = helper.nvim hover = helper.plugin.getHandler().hover }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return hoverResult } })) }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) async function getDocumentText(): Promise { let lines = await nvim.call('getbufline', ['coc://document', 1, '$']) as string[] return lines.join('\n') } describe('Hover', () => { describe('utils', () => { it('should addDocument', async () => { let docs: Documentation[] = [] addDocument(docs, '', '') expect(docs.length).toBe(0) }) it('should check documentation', async () => { expect(isDocumentation(undefined)).toBe(false) expect(isDocumentation({})).toBe(false) expect(isDocumentation({ filetype: '', content: '' })).toBe(true) }) it('should readLines', async () => { let res = await readLines('file:///not_exists', 0, 1) expect(res).toEqual([]) }) it('should addDefinitions', async () => { let hovers = [] let range = Range.create(0, 0, 0, 0) await addDefinitions(hovers, [undefined, {} as any, { targetUri: 'file:///not_exists', targetRange: range, targetSelectionRange: range }], '') expect(hovers.length).toBe(0) let file = await createTmpFile(' foo\nbar\n', disposables) range = Range.create(0, 0, 300, 0) await addDefinitions(hovers, [{ targetUri: URI.file(file).toString(), targetRange: range, targetSelectionRange: range }], '') expect(hovers.length).toBe(1) }) }) describe('onHover', () => { it('should return false when hover not found', async () => { hoverResult = null let res = await hover.onHover('preview') expect(res).toBe(false) }) it('should show MarkupContent hover', async () => { helper.updateConfiguration('hover.target', 'preview') hoverResult = { contents: { kind: 'plaintext', value: 'my hover' } } await helper.doAction('doHover') let res = await getDocumentText() expect(res).toMatch('my hover') }) it('should merge hover results', async () => { hoverResult = { contents: { kind: 'plaintext', value: 'my hover' } } disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return null } })) disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return { contents: { kind: 'plaintext', value: 'my hover' } } } })) let doc = await workspace.document let hovers = await languages.getHover(doc.textDocument, Position.create(0, 0), CancellationToken.None) expect(hovers.length).toBe(1) }) it('should show MarkedString hover', async () => { hoverResult = { contents: 'string hover' } disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return { contents: { language: 'typescript', value: 'language hover' } } } })) await hover.onHover('preview') let res = await getDocumentText() expect(res).toMatch('string hover') expect(res).toMatch('language hover') }) it('should show MarkedString hover array', async () => { hoverResult = { contents: ['foo', { language: 'typescript', value: 'bar' }] } await hover.onHover('preview') let res = await getDocumentText() expect(res).toMatch('foo') expect(res).toMatch('bar') }) it('should highlight hover range', async () => { await nvim.setLine('var') await nvim.command('normal! 0') hoverResult = { contents: ['foo'], range: Range.create(0, 0, 0, 3) } await hover.onHover('preview') let res = await nvim.call('getmatches') as any[] expect(res.length).toBe(1) expect(res[0].group).toBe('CocHoverRange') await helper.waitValue(async () => { let res = await nvim.call('getmatches') as any[] return res.length }, 0) }) }) describe('previewHover', () => { it('should echo hover message', async () => { hoverResult = { contents: ['foo'] } let res = await hover.onHover('echo') expect(res).toBe(true) let msg = await helper.getCmdline() expect(msg).toMatch('foo') }) it('should show hover in float window', async () => { hoverResult = { contents: { kind: 'markdown', value: '```typescript\nconst foo:number\n```' } } await hover.onHover('float') let win = await helper.getFloat() expect(win).toBeDefined() let lines = await nvim.eval(`getbufline(winbufnr(${win.id}),1,'$')`) expect(lines).toEqual(['const foo:number']) }) }) describe('getHover', () => { it('should get hover from MarkedString array', async () => { hoverResult = { contents: ['foo', { language: 'typescript', value: 'bar' }] } disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return { contents: { language: 'typescript', value: 'MarkupContent hover' } } } })) disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return { contents: MarkedString.fromPlainText('MarkedString hover') } } })) let res = await helper.doAction('getHover') expect(res.includes('foo')).toBe(true) expect(res.includes('bar')).toBe(true) expect(res.includes('MarkupContent hover')).toBe(true) expect(res.includes('MarkedString hover')).toBe(true) }) it('should filter empty hover message', async () => { hoverResult = { contents: [''] } disposables.push(languages.registerHoverProvider([{ language: '*' }], { provideHover: (_doc, _pos, _token) => { return { contents: { kind: MarkupKind.PlainText, value: 'value' } } } })) let res = await hover.getHover({ line: 1, col: 2 }) expect(res).toEqual(['value']) }) it('should throw when buffer not attached', async () => { await expect(async () => { await hover.getHover({ bufnr: 999, line: 1, col: 2 }) }).rejects.toThrow(/not exists/) }) }) describe('definitionHover', () => { it('should load definition from buffer', async () => { hoverResult = { contents: 'string hover' } let doc = await helper.createDocument() await nvim.call('cursor', [1, 1]) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition() { return [{ targetUri: doc.uri, targetRange: Range.create(0, 0, 1, 3), targetSelectionRange: Range.create(0, 0, 0, 3), }] } })) await helper.doAction('definitionHover', 'preview') let res = await getDocumentText() expect(res).toBe('string hover\n\nfoo\nbar') }) it('should load definition link from file', async () => { let fsPath = await createTmpFile('foo\nbar\n') hoverResult = { contents: 'string hover', range: Range.create(0, 0, 0, 3) } let doc = await helper.createDocument() await nvim.call('cursor', [1, 1]) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition() { return [{ targetUri: URI.file(fsPath).toString(), targetRange: Range.create(0, 0, 1, 3), targetSelectionRange: Range.create(0, 0, 0, 3), }] } })) await hover.definitionHover('preview') let res = await getDocumentText() expect(res).toBe('string hover\n\nfoo\nbar') }) it('should return false when hover not found', async () => { hoverResult = undefined let res = await hover.definitionHover('float') expect(res).toBe(false) }) }) }) ================================================ FILE: src/__tests__/handler/index.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable, SymbolKind } from 'vscode-languageserver-protocol' import commands from '../../commands' import Handler from '../../handler/index' import { toDocumentation } from '../../handler/util' import { ProviderName } from '../../languages' import { disposeAll } from '../../util' import helper from '../helper' let nvim: Neovim let handler: Handler let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim handler = (helper.plugin as any).handler }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('Handler', () => { describe('util', () => { it('should to documentation', () => { expect(toDocumentation('doc')).toEqual({ content: 'doc', filetype: 'txt' }) expect(toDocumentation({ kind: 'markdown', value: 'doc' })).toEqual({ content: 'doc', filetype: 'markdown' }) }) }) describe('hasProvider', () => { it('should check provider for document', async () => { let res = await helper.doAction('hasProvider', 'definition') expect(res).toBe(false) await nvim.command(`edit +setl\\ buftype=nofile foo`) res = await handler.hasProvider('formatOnType') expect(res).toBe(false) }) }) describe('getIcon', () => { it('should get icon', () => { helper.updateConfiguration('suggest.completionItemKindLabels', { default: 'd' }) let res = handler.getIcon(SymbolKind.Array) expect(res).toBeDefined() res = handler.getIcon('a' as any) expect(res.text).toBe('d') }) }) describe('commands', () => { it('should open url', async () => { let fn = jest.fn() let spy = jest.spyOn(nvim, 'call').mockImplementation(() => { fn() return null }) await commands.executeCommand('vscode.open', 'http://www.example.com') spy.mockRestore() expect(fn).toHaveBeenCalled() }) it('should restart', async () => { let fn = jest.fn() let spy = jest.spyOn(nvim, 'command').mockImplementation(() => { fn() return null }) await commands.executeCommand('workbench.action.reloadWindow') spy.mockRestore() expect(fn).toHaveBeenCalled() }) }) describe('checkProvider', () => { it('should throw error when provider not found', async () => { let doc = await helper.createDocument() let err try { handler.checkProvider(ProviderName.Definition, doc.textDocument) } catch (e) { err = e } expect(err).toBeDefined() }) }) describe('withRequestToken', () => { it('should cancel previous request when called again', async () => { let cancelled = false let p = handler.withRequestToken('test', token => { return new Promise(s => { token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) s(undefined) }) let timer = setTimeout(() => { s(undefined) }, 3000) }) }, false) setTimeout(async () => { await handler.withRequestToken('test', () => { return Promise.resolve(undefined) }, false) }, 50) await p expect(cancelled).toBe(true) }) it('should cancel request on insert start', async () => { let cancelled = false let p = handler.withRequestToken('test', token => { return new Promise(s => { token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) s(undefined) }) let timer = setTimeout(() => { s(undefined) }, 3000) }) }, false) await nvim.input('i') await p expect(cancelled).toBe(true) }) }) }) ================================================ FILE: src/__tests__/handler/inlayHint.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CancellationTokenSource, Disposable, InlayHint, InlayHintKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import commands from '../../commands' import InlayHintHandler from '../../handler/inlayHint/index' import languages from '../../languages' import { InlayHintWithProvider, isInlayHint, isValidInlayHint, sameHint } from '../../provider/inlayHintManager' import { disposeAll } from '../../util' import { CancellationError } from '../../util/errors' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let handler: InlayHintHandler let disposables: Disposable[] = [] let ns: number beforeAll(async () => { await helper.setup() nvim = helper.nvim handler = helper.plugin.getHandler().inlayHintHandler ns = await nvim.createNamespace('coc-inlayHint') }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) async function registerProvider(content: string): Promise { let doc = await workspace.document let disposable = languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: (document, range) => { let content = document.getText(range) let lines = content.split(/\r?\n/) let hints: InlayHint[] = [] for (let i = 0; i < lines.length; i++) { let line = lines[i] if (!line.length) continue let parts = line.split(/\s+/) let kind: InlayHintKind = i == 0 ? InlayHintKind.Type : InlayHintKind.Parameter hints.push(...parts.map(s => InlayHint.create(Position.create(range.start.line + i, line.length), s, kind))) } return hints } }) await helper.wait(10) await doc.buffer.setLines(content.split(/\n/), { start: 0, end: -1 }) await doc.synchronize() return disposable } async function waitRefresh(bufnr: number) { let buf = handler.getItem(bufnr) return new Promise((resolve, reject) => { let timer = setTimeout(() => { reject(new Error('not refresh after 1s')) }, 1000) buf.onDidRefresh(() => { clearTimeout(timer) resolve() }) }) } describe('InlayHint', () => { describe('utils', () => { it('should check same hint', () => { let hint = InlayHint.create(Position.create(0, 0), 'foo') expect(sameHint(hint, InlayHint.create(Position.create(0, 0), 'bar'))).toBe(false) expect(sameHint(hint, InlayHint.create(Position.create(0, 0), [{ value: 'foo' }]))).toBe(true) }) it('should check valid hint', () => { let hint = InlayHint.create(Position.create(0, 0), 'foo') expect(isValidInlayHint(hint, Range.create(0, 0, 1, 0))).toBe(true) expect(isValidInlayHint(InlayHint.create(Position.create(0, 0), ''), Range.create(0, 0, 1, 0))).toBe(false) expect(isValidInlayHint(InlayHint.create(Position.create(3, 0), 'foo'), Range.create(0, 0, 1, 0))).toBe(false) expect(isValidInlayHint({ label: 'f' } as any, Range.create(0, 0, 1, 0))).toBe(false) }) it('should check inlayHint instance', async () => { expect(isInlayHint(null)).toBe(false) let position = Position.create(0, 0) expect(isInlayHint({ position, label: null })).toBe(false) expect(isInlayHint({ position, label: [{ value: '' }] })).toBe(true) }) }) describe('provideInlayHints', () => { // not fail like VSCode it('should not throw when failed', async () => { disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return Promise.reject(new Error('Test failure')) } })) let doc = await workspace.document let tokenSource = new CancellationTokenSource() await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token) }) it('should merge provider results', async () => { disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return [InlayHint.create(Position.create(0, 0), 'foo')] } })) disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return [ InlayHint.create(Position.create(0, 0), 'foo'), InlayHint.create(Position.create(1, 0), 'bar'), InlayHint.create(Position.create(5, 0), 'bad')] } })) disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return null } })) await helper.wait(10) let doc = await workspace.document let tokenSource = new CancellationTokenSource() let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 3, 0), tokenSource.token) expect(res.length).toBe(2) }) it('should not throw when provider return null', async () => { disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { throw new CancellationError() } })) let doc = await workspace.document let item = handler.getItem(doc.bufnr) item.clearCache() await item.renderRange([0, 1], CancellationToken.Cancelled) }) it('should resolve inlay hint', async () => { disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return [InlayHint.create(Position.create(0, 0), 'foo')] }, resolveInlayHint: hint => { hint.tooltip = 'tooltip' return hint } })) await helper.wait(10) let doc = await workspace.document let tokenSource = new CancellationTokenSource() let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token) let resolved = await languages.resolveInlayHint(res[0], tokenSource.token) expect(resolved.tooltip).toBe('tooltip') resolved = await languages.resolveInlayHint(resolved, tokenSource.token) expect(resolved.tooltip).toBe('tooltip') }) it('should not resolve when cancelled', async () => { disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return [InlayHint.create(Position.create(0, 0), 'foo')] }, resolveInlayHint: (hint, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) resolve(null) }) let timer = setTimeout(() => { resolve(Object.assign({}, hint, { tooltip: 'tooltip' })) }, 200) }) } })) await helper.wait(10) let doc = await workspace.document let tokenSource = new CancellationTokenSource() let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token) let p = languages.resolveInlayHint(res[0], tokenSource.token) tokenSource.cancel() let resolved = await p expect(resolved.tooltip).toBeUndefined() }) }) describe('env & options', () => { it('should not enabled when disabled by configuration', async () => { helper.updateConfiguration('inlayHint.filetypes', [], disposables) let doc = await workspace.document let item = handler.getItem(doc.bufnr) item.clearVirtualText() expect(item.enabled).toBe(false) helper.updateConfiguration('inlayHint.filetypes', ['dos'], disposables) doc = await helper.createDocument() item = handler.getItem(doc.bufnr) expect(item.enabled).toBe(false) }) }) describe('configuration', () => { it('should refresh on insert mode', async () => { helper.updateConfiguration('inlayHint.refreshOnInsertMode', true, disposables) let doc = await helper.createDocument() let disposable = await registerProvider('foo\nbar') disposables.push(disposable) await nvim.input('i') await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'baz\n')]) await waitRefresh(doc.bufnr) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) let obj = markers[0][3].virt_text expect(obj).toEqual([['baz', 'CocInlayHintType']]) expect(markers[1][3].virt_text).toEqual([['foo', 'CocInlayHintParameter']]) }) it('should disable parameter inlayHint', async () => { helper.updateConfiguration('inlayHint.enableParameter', false, disposables) let doc = await helper.createDocument() let disposable = await registerProvider('foo\nbar') disposables.push(disposable) await waitRefresh(doc.bufnr) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(1) }) it('should enable & disable inlayHint', async () => { let doc = await helper.createDocument() let disposable = await registerProvider('foo\nbar') disposables.push(disposable) await waitRefresh(doc.bufnr) helper.updateConfiguration('inlayHint.enable', false) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(0) helper.updateConfiguration('inlayHint.enable', true) }) it('should change position to eol', async () => { helper.updateConfiguration('inlayHint.position', 'eol', disposables) let doc = await helper.createDocument() let disposable = await registerProvider('foo\nbar') disposables.push(disposable) await waitRefresh(doc.bufnr) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(2) for (const m of markers) { let detail = m[3] expect(detail['virt_text_pos']).toBe('eol') } }) it('should truncate hint label when exceeding maximumLength', async () => { helper.updateConfiguration('inlayHint.maximumLength', 13, disposables) let doc = await helper.createDocument() let disposable = languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return [ InlayHint.create(Position.create(0, 0), 'firstLabel', InlayHintKind.Type), InlayHint.create(Position.create(0, 3), 'secondLabel', InlayHintKind.Type), ] } }) disposables.push(disposable) await doc.buffer.setLines(['foo'], { start: 0, end: -1 }) await doc.synchronize() await waitRefresh(doc.bufnr) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(2) let first = markers[0][3].virt_text expect(first).toEqual([['firstLabel', 'CocInlayHintType']]) let second = markers[1][3].virt_text expect(second).toEqual([['sec…', 'CocInlayHintType']]) }) it('should not truncate hint label when maximumLength is 0', async () => { helper.updateConfiguration('inlayHint.maximumLength', 0, disposables) let doc = await helper.createDocument() let disposable = languages.registerInlayHintsProvider([{ language: '*' }], { provideInlayHints: () => { return [ InlayHint.create(Position.create(0, 0), 'firstLabel', InlayHintKind.Type), InlayHint.create(Position.create(0, 3), 'secondLabel', InlayHintKind.Type), ] } }) disposables.push(disposable) await doc.buffer.setLines(['foo'], { start: 0, end: -1 }) await doc.synchronize() await waitRefresh(doc.bufnr) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(2) let first = markers[0][3].virt_text expect(first).toEqual([['firstLabel', 'CocInlayHintType']]) let second = markers[1][3].virt_text expect(second).toEqual([['secondLabel', 'CocInlayHintType']]) }) }) describe('inlayHint setState', () => { it('should not throw when buffer not exists', async () => { handler.setState('toggle', 9) await commands.executeCommand('document.toggleInlayHint', 9) }) it('should show message when inlayHint not supported', async () => { let doc = await workspace.document handler.setState('toggle', doc.bufnr) let cmdline = await helper.getCmdline() expect(cmdline).toMatch(/not\sfound/) }) it('should show message when not enabled', async () => { helper.updateConfiguration('inlayHint.filetypes', [], disposables) let doc = await helper.createDocument() let disposable = await registerProvider('') disposables.push(disposable) handler.setState('toggle', doc.bufnr) let cmdline = await helper.getCmdline() expect(cmdline).toMatch(/not\senabled/) }) it('should toggle inlayHints', async () => { let doc = await helper.createDocument() let disposable = await registerProvider('foo\nbar') disposables.push(disposable) handler.setState('toggle', doc.bufnr) handler.setState('toggle', doc.bufnr) await helper.waitValue(async () => { let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) return markers.length }, 2) }) it('should enable & disable inlayHint', async () => { let doc = await helper.createDocument() let disposable = await registerProvider('foo\nbar') disposables.push(disposable) await commands.executeCommand('document.disableInlayHint') await commands.executeCommand('document.enableInlayHint') let item = handler.getItem(doc.bufnr) expect(item.enabled).toBe(true) }) }) describe('render()', () => { it('should refresh on vim mode', async () => { let doc = await workspace.document await nvim.setLine('foo bar') let item = handler.getItem(doc.bufnr) let r = Range.create(0, 0, 1, 0) item.setVirtualText(r, []) let hint: InlayHintWithProvider = { label: 'string', position: Position.create(0, 0), providerId: '' } let paddingHint: InlayHintWithProvider = { label: 'string', position: Position.create(0, 3), providerId: '', paddingLeft: true, paddingRight: true } item.setVirtualText(r, [hint, paddingHint]) await helper.waitValue(async () => { let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) return markers.length }, 2) }) it('should not refresh when languageId not match', async () => { let doc = await workspace.document disposables.push(languages.registerInlayHintsProvider([{ language: 'javascript' }], { provideInlayHints: () => { let hint = InlayHint.create(Position.create(0, 0), 'foo') return [hint] } })) await nvim.setLine('foo') await doc.synchronize() await helper.wait(30) let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(0) }) it('should refresh on text change', async () => { let buf = await nvim.buffer let disposable = await registerProvider('foo') disposables.push(disposable) await waitRefresh(buf.id) await buf.setLines(['a', 'b', 'c'], { start: 0, end: -1 }) await waitRefresh(buf.id) let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(3) let item = handler.getItem(buf.id) await item.render() expect(item.current.length).toBe(3) }) it('should refresh on insert leave', async () => { let doc = await helper.createDocument() let buf = doc.buffer let disposable = await registerProvider('foo') disposables.push(disposable) await nvim.input('i') await helper.wait(10) await buf.setLines(['a', 'b', 'c'], { start: 0, end: -1 }) await helper.wait(30) let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(0) await nvim.input('') await waitRefresh(doc.bufnr) markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(3) }) it('should refresh on provider dispose', async () => { let buf = await nvim.buffer let disposable = await registerProvider('foo bar') await waitRefresh(buf.id) disposable.dispose() let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(0) let item = handler.getItem(buf.id) expect(item.current.length).toBe(0) await item.render() expect(item.current.length).toBe(0) }) it('should refresh on scroll', async () => { let arr = new Array(workspace.env.lines * 5) let content = arr.fill('foo').join('\n') let buf = await nvim.buffer let disposable = await registerProvider(content) disposables.push(disposable) await waitRefresh(buf.id) let item = handler.getItem(buf.id) item.clearVirtualText() item.clearCache() await nvim.command('normal! G') await waitRefresh(buf.id) let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) let len = markers.length await nvim.command('normal! gg') await waitRefresh(buf.id) await nvim.command('normal! G') markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBeGreaterThan(len) }) it('should cancel previous render', async () => { let buf = await nvim.buffer let disposable = await registerProvider('foo') disposables.push(disposable) await waitRefresh(buf.id) let item = handler.getItem(buf.id) await item.render() await item.render() expect(item.current.length).toBe(1) }) it('should resend request on CancellationError', async () => { let called = 0 let disposable = languages.registerInlayHintsProvider([{ language: 'vim' }], { provideInlayHints: () => { called++ if (called == 1) { throw new CancellationError() } return [] } }) disposables.push(disposable) await helper.wait(10) let filepath = await createTmpFile('a\n\b\nc\n', disposables) await helper.createDocument(filepath) await nvim.command('setfiletype vim') await helper.waitValue(() => called, 2) }) }) }) ================================================ FILE: src/__tests__/handler/inline.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { FormattingOptions, InlineCompletionItem, Position, Range, TextEdit } from 'vscode-languageserver-types' import commands from '../../commands' import sources from '../../completion/sources' import { CompleteOption, CompleteResult, ExtendedCompleteItem } from '../../completion/types' import events from '../../events' import InlineCompletion, { checkInsertedAtBeginning, formatInsertText, getInserted, getInsertText, getPumInserted, InlineSession } from '../../handler/inline' import languages from '../../languages' import { Disposable } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let inlineCompletion: InlineCompletion let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim inlineCompletion = helper.plugin.handler.inlineCompletion }) afterAll(async () => { await helper.shutdown() }) describe('InlineCompletion', () => { afterEach(async () => { jest.clearAllMocks() inlineCompletion['_inserted'] = undefined await helper.reset() disposables.forEach(d => d.dispose()) disposables = [] if (inlineCompletion.session) { inlineCompletion.cancel() } }) function mockInlineInsert(returnValue: boolean): void { // Mock nvim calls let fn = nvim.call nvim.call = jest.fn().mockImplementation((method, ...args) => { if (method === 'coc#inline#_insert') return Promise.resolve(returnValue) if (method === 'coc#inline#clear') return Promise.resolve() return fn.apply(nvim, [method, ...args] as any) }) } describe('events', () => { it('should trigger on document change', async () => { helper.updateConfiguration('inline.autoTrigger', true, disposables) await nvim.command('startinsert') let doc = await helper.createDocument() let mockProvider = jest.fn() let providerDisposable = languages.registerInlineCompletionItemProvider( [{ language: '*' }], { provideInlineCompletionItems: mockProvider } ) disposables.push(providerDisposable) const spy = jest.spyOn(inlineCompletion, 'trigger') await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'test')]) expect(spy).toHaveBeenCalledTimes(1) }) it('should cancel on buffer unload', async () => { let doc = await workspace.document const item: InlineCompletionItem = { insertText: 'completion text', range: Range.create(0, 5, 0, 5) } inlineCompletion['bufnr'] = doc.bufnr inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 5), [item]) const spy = jest.spyOn(inlineCompletion, 'cancel') await nvim.command('bwipeout!') workspace.documentsManager.detachBuffer(doc.bufnr) expect(spy).toHaveBeenCalledTimes(1) }) it('should not cancel when mode changed from i to ic', async () => { let doc = await workspace.document const item: InlineCompletionItem = { insertText: 'completion text', range: Range.create(0, 5, 0, 5) } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 5), [item]) const spy = jest.spyOn(inlineCompletion, 'cancel') await events.fire('ModeChanged', [{ old_mode: 'i', new_mode: 'ic' }]) expect(spy).not.toHaveBeenCalled() }) it('should trigger on pum navigate', async () => { let doc = await workspace.document let providerDisposable = languages.registerInlineCompletionItemProvider( [{ language: '*' }], { provideInlineCompletionItems: () => { return Promise.resolve([{ insertText: 'bar()' }]) } } ) disposables.push(providerDisposable) disposables.push(sources.createSource({ name: 'test', doComplete: (_opt: CompleteOption): Promise> => new Promise(resolve => { resolve({ items: [{ word: 'foo' }, { word: 'bar' }] }) }) })) let mode = await nvim.mode if (mode.mode !== 'i') { await nvim.command('startinsert') } nvim.call('coc#start', { source: 'test' }, true) await helper.waitPopup() await nvim.call('coc#pum#_navigate', [1, 1]) await helper.waitFor('coc#inline#visible', [], 1) await inlineCompletion.accept(doc.bufnr) let line = await nvim.line expect(line).toBe('bar()') }) it('should accept snippet inlineCompletion on pum navigate', async () => { let doc = await workspace.document // Set up a line to work with await nvim.setLine('prefix ') await doc.patchChange() // Register inline completion provider that returns snippet items let providerDisposable = languages.registerInlineCompletionItemProvider( [{ language: '*' }], { provideInlineCompletionItems: () => { return Promise.resolve([{ insertText: { value: 'snippet ${1:param1} ${2:param2}', kind: 'snippet' } }]) } } ) disposables.push(providerDisposable) // Create a completion source disposables.push(sources.createSource({ name: 'snippet-test', doComplete: (_opt: CompleteOption): Promise> => new Promise(resolve => { resolve({ items: [{ word: 'snip' }, { word: 'snippet' }] }) }) })) // Start insert mode if not already let mode = await nvim.mode if (mode.mode !== 'i') { await nvim.command('startinsert') } // Move cursor to end of line await nvim.call('cursor', [1, 8]) // After "prefix " // Start completion nvim.call('coc#start', { source: 'snippet-test' }, true) await helper.waitPopup() // Navigate in popup to trigger inline completion await nvim.call('coc#pum#_navigate', [1, 1]) await helper.waitFor('coc#inline#visible', [], 1) // Spy on executeCommand to check if snippet command is executed const executeCommandSpy = jest.spyOn(commands, 'executeCommand') // Accept the completion let res = await inlineCompletion.accept(doc.bufnr) // Check result expect(res).toBe(true) expect(inlineCompletion.session).toBeUndefined() // Session should be cleared expect(executeCommandSpy).toHaveBeenCalledWith( 'editor.action.insertSnippet', expect.objectContaining({ range: expect.any(Object), newText: ' ${1:param1} ${2:param2}' }) ) // Cleanup executeCommandSpy.mockRestore() await inlineCompletion.accept(doc.bufnr) let line = await nvim.line expect(line).toBe('prefix snippet param1 param2') }) it('should adjust range based on _inserted in insertVtext', async () => { let doc = await workspace.document // Set up document with "prefix in" where "in" is what would be inserted by pum await nvim.setLine('prefix in') await doc.patchChange() // Create a completion item with range covering "in" and insertText that extends it const item: InlineCompletionItem = { insertText: 'inserted text', range: Range.create(0, 7, 0, 7) } // Create session with cursor at end of "in" inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 9), [item]) // Set _inserted to simulate pum insertion inlineCompletion['_inserted'] = 'in' // Mock inline insert mockInlineInsert(true) // Call insertVtext await inlineCompletion.insertVtext(item) // // Verify that vtext starts after "in" expect(inlineCompletion.session.vtext).toBe('serted text') // Check that the range was adjusted in the call to coc#inline#_insert // The col should be 10 (byte index of position after "in" + 1) expect(nvim.call).toHaveBeenCalledWith( 'coc#inline#_insert', [doc.bufnr, 0, 10, ['serted text'], ''] ) await inlineCompletion.accept(doc.bufnr) let line = await nvim.line expect(line).toBe('prefix inserted text') }) }) describe('insertVtext()', () => { it('should insert virtual text successfully', async () => { let doc = await workspace.document await nvim.setLine('fooba') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'completion text', range: Range.create(0, 5, 0, 5) } await inlineCompletion.insertVtext(undefined) inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 5), [item]) mockInlineInsert(true) await inlineCompletion.insertVtext(item) expect(nvim.call).toHaveBeenCalledWith( 'coc#inline#_insert', [doc.bufnr, 0, 6, ['completion text'], ''] ) expect(inlineCompletion.session.vtext).toBe('completion text') }) it('should show index when multiple items exist', async () => { let doc = await workspace.document const item1: InlineCompletionItem = { insertText: 'first' } const item2: InlineCompletionItem = { insertText: 'second' } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 0), [item1, item2]) mockInlineInsert(true) await inlineCompletion.insertVtext(item1) expect(nvim.call).toHaveBeenCalledWith( 'coc#inline#_insert', [doc.bufnr, 0, 1, ['first'], '(1/2)'] ) }) it('should handle item with non-empty range', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'complete')]) const item: InlineCompletionItem = { insertText: 'complete method()', range: Range.create(0, 0, 0, 8) // Assume "complete" is already typed } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 8), [item]) mockInlineInsert(true) await inlineCompletion.insertVtext(item) expect(inlineCompletion.session.vtext).toBe(' method()') }) it('should handle cursor in middle of completion range', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'compl()')]) const item: InlineCompletionItem = { insertText: 'completeMethod()', range: Range.create(0, 0, 0, 7) // "compl()" } // Cursor is at "compl|()" inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 5), [item]) mockInlineInsert(true) await inlineCompletion.insertVtext(item) expect(inlineCompletion.session.vtext).toBe('eteMethod') }) it('should handle cursor at the end of completion range but text does not match', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'initialText')]) const item: InlineCompletionItem = { insertText: 'initialTextReplacement', range: Range.create(0, 0, 0, 11) // "initialText" } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 11), [item]) mockInlineInsert(true) await inlineCompletion.insertVtext(item) expect(inlineCompletion.session.vtext).toBe('Replacement') }) it('should handle item range where text after cursor does not match end of insertText', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'prefixMismatchSuffix')]) const item: InlineCompletionItem = { insertText: 'prefixReplacementSuffix', range: Range.create(0, 0, 0, 20) // "prefixMismatchSuffix" } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 6), [item]) mockInlineInsert(true) await inlineCompletion.insertVtext(item) expect(inlineCompletion.session.vtext).toBe('ReplacementSuffix') }) it('should clean up when insertion fails', async () => { let doc = await workspace.document const item: InlineCompletionItem = { insertText: 'text' } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 5), [item]) mockInlineInsert(false) await inlineCompletion.insertVtext(item) expect(inlineCompletion.session).toBeUndefined() let visible = await inlineCompletion.visible() expect(visible).toBe(false) }) it('should handle multiline completions', async () => { let doc = await workspace.document const item: InlineCompletionItem = { insertText: 'line1\nline2\nline3', } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 0), [item]) mockInlineInsert(true) await inlineCompletion.insertVtext(item) expect(nvim.call).toHaveBeenCalledWith( 'coc#inline#_insert', [doc.bufnr, 0, 1, 'line1\nline2\nline3'.split('\n'), ""] ) expect(inlineCompletion.session.vtext).toBe('line1\nline2\nline3') }) }) describe('accept()', () => { it('should not accept when no selected item', async () => { let doc = await workspace.document const item: InlineCompletionItem = { insertText: 'bar', } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 3), [item], -1, 'bar') let res = await helper.doAction('inlineAccept', doc.bufnr, 'all') expect(res).toBe(false) }) it('should accept completion and apply TextEdit', async () => { let doc = await workspace.document await nvim.setLine('foo') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'bar', } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 3), [item], 0, 'bar') const applyEditsSpy = jest.spyOn(doc, 'applyEdits') const moveToSpy = jest.spyOn(window, 'moveTo') await inlineCompletion.accept(doc.bufnr) expect(applyEditsSpy).toHaveBeenCalledWith( [TextEdit.replace(Range.create(0, 3, 0, 3), 'bar')], false, false ) expect(moveToSpy).toHaveBeenCalledWith(Position.create(0, 6)) // 'foo' + 'bar' expect(inlineCompletion.session).toBeUndefined() // Session should be cleared const content = await doc.buffer.lines expect(content[0]).toBe('foobar') }) it('should accept completion with a specific range', async () => { let doc = await workspace.document await nvim.setLine('prefixsuffix') // prefix|suffix await doc.patchChange() const item: InlineCompletionItem = { insertText: 'replacement', range: Range.create(0, 6, 0, 6) // Replacing nothing, just inserting at cursor } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 6), [item], 0, 'replacement') const applyEditsSpy = jest.spyOn(doc, 'applyEdits') const moveToSpy = jest.spyOn(window, 'moveTo') await inlineCompletion.accept(doc.bufnr) // The range in item is used for TextEdit.replace expect(applyEditsSpy).toHaveBeenCalledWith( [TextEdit.replace(Range.create(0, 6, 0, 6), 'replacement')], false, false ) expect(moveToSpy).toHaveBeenCalledWith(Position.create(0, 17)) // prefixreplacement|suffix const content = await doc.buffer.lines expect(content[0]).toBe('prefixreplacementsuffix') }) it('should accept snippet completion item', async () => { let doc = await workspace.document await nvim.setLine('before') await doc.patchChange() const snippetString = 'snippet ${1:one} then ${2:two}' const item: InlineCompletionItem = { insertText: { kind: 'snippet', value: snippetString } } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 6), [item]) inlineCompletion.session.vtext = 'snippet one then two' // What vtext might show let res = await inlineCompletion.accept(doc.bufnr) expect(inlineCompletion.session).toBeUndefined() expect(res).toBe(true) }) it('should accept word as kind', async () => { let doc = await workspace.document await nvim.setLine('prefix ') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'firstWord secondWord' } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 7), [item]) inlineCompletion.session.vtext = 'firstWord secondWord' // Mock isWord const originalIsWord = doc.isWord doc.isWord = jest.fn(char => /[a-zA-Z]/.test(char)) await inlineCompletion.accept(doc.bufnr, 'word') expect(inlineCompletion.session).toBeUndefined() const content = await doc.buffer.lines expect(content[0]).toBe('prefix firstWord') doc.isWord = originalIsWord // Restore original }) it('should accept word as kind with no clear word boundary', async () => { let doc = await workspace.document await nvim.setLine('prefix') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'onlyword' // No spaces or punctuation } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 6), [item]) inlineCompletion.session.vtext = 'onlyword' const originalIsWord = doc.isWord doc.isWord = jest.fn(char => /[a-zA-Z]/.test(char)) const applyEditsSpy = jest.spyOn(doc, 'applyEdits') await inlineCompletion.accept(doc.bufnr, 'word') expect(applyEditsSpy).toHaveBeenCalledWith( [TextEdit.replace(Range.create(0, 6, 0, 6), 'onlyword')], false, false ) const content = await doc.buffer.lines expect(content[0]).toBe('prefixonlyword') doc.isWord = originalIsWord }) it('should accept line as kind', async () => { let doc = await workspace.document await nvim.setLine('prefix ') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'firstLine\nsecondLine' } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 7), [item]) inlineCompletion.session.vtext = 'firstLine\nsecondLine' await inlineCompletion.accept(doc.bufnr, 'line') expect(inlineCompletion.session).toBeUndefined() const content = await doc.buffer.lines expect(content[0]).toBe('prefix firstLine') }) it('should accept line as kind with single line insertText', async () => { let doc = await workspace.document await nvim.setLine('prefix ') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'singleLineText' } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 7), [item]) inlineCompletion.session.vtext = 'singleLineText' const applyEditsSpy = jest.spyOn(doc, 'applyEdits') await inlineCompletion.accept(doc.bufnr, 'line') expect(applyEditsSpy).toHaveBeenCalledWith( [TextEdit.replace(Range.create(0, 7, 0, 7), 'singleLineText')], false, false ) const content = await doc.buffer.lines expect(content[0]).toBe('prefix singleLineText') }) it('should not throw when completion command throws error', async () => { let doc = await workspace.document await nvim.setLine('test') await doc.patchChange() const item: InlineCompletionItem = { insertText: 'text', command: { command: 'test.command', title: 'Test' } } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 4), [item]) inlineCompletion.session.vtext = 'text' let res = await inlineCompletion.accept(doc.bufnr) expect(inlineCompletion.session).toBeUndefined() // Session should still be cleared expect(res).toBe(true) }) it('should do nothing if bufnr does not match session bufnr', async () => { let doc = await workspace.document const item: InlineCompletionItem = { insertText: 'text' } inlineCompletion.session = new InlineSession(doc.bufnr, Position.create(0, 0), [item]) inlineCompletion.session.vtext = 'text' // Simulate vtext is shown let res = await inlineCompletion.accept(doc.bufnr + 1) // Different bufnr expect(res).toBe(false) expect(inlineCompletion.session).toBeDefined() // Session should not be cleared }) }) describe('trigger()', () => { let mockProvider: jest.Mock let providerDisposable: Disposable beforeEach(() => { mockProvider = jest.fn() providerDisposable = languages.registerInlineCompletionItemProvider( [{ language: '*' }], { provideInlineCompletionItems: mockProvider } ) disposables.push(providerDisposable) // Mock getCurrentState to simulate insert mode jest.spyOn(helper.plugin.handler, 'getCurrentState').mockResolvedValue({ doc: workspace.getDocument(workspace.bufnr), position: Position.create(0, 0), mode: 'i', winid: 1, } as any) mockInlineInsert(true) // Assume inline insert will succeed for trigger tests }) afterEach(() => { if (providerDisposable) providerDisposable.dispose() }) it('should not trigger if no provider is registered for the document', async () => { providerDisposable.dispose() // Unregister the provider let doc = await workspace.document await helper.doAction('inlineTrigger', doc.bufnr) expect(mockProvider).not.toHaveBeenCalled() expect(inlineCompletion.session).toBeUndefined() }) it('should return false when not supported', async () => { let doc = await workspace.document let spy = jest.spyOn(workspace, 'has').mockReturnValue(false) // Simulate inline completion not supported let res = await inlineCompletion.trigger(doc.bufnr) expect(res).toBe(false) expect(inlineCompletion.session).toBeUndefined() expect(inlineCompletion.selected).toBeUndefined() spy.mockRestore() }) it('should not trigger if provider returns no items (autoTrigger: true)', async () => { mockProvider.mockResolvedValue([]) const spy = jest.spyOn(window, 'showWarningMessage') await commands.executeCommand('editor.action.triggerInlineCompletion', { autoTrigger: true }) expect(mockProvider).toHaveBeenCalled() expect(inlineCompletion.session).toBeUndefined() expect(spy).not.toHaveBeenCalled() // No warning for autoTrigger }) it('should show warning if provider returns no items (autoTrigger: false)', async () => { mockProvider.mockResolvedValue([]) let doc = await workspace.document const spy = jest.spyOn(window, 'showWarningMessage') await inlineCompletion.trigger(doc.bufnr, { autoTrigger: false }) expect(mockProvider).toHaveBeenCalled() expect(inlineCompletion.session).toBeUndefined() expect(spy).toHaveBeenCalledWith('No inline completion items from provider.') }) it('should trigger and create session if provider returns items', async () => { const item: InlineCompletionItem = { insertText: 'suggested' } mockProvider.mockResolvedValue([item]) let doc = await workspace.document await inlineCompletion.trigger(doc.bufnr) expect(mockProvider).toHaveBeenCalled() expect(inlineCompletion.session).toBeDefined() expect(inlineCompletion.session.items).toEqual([item]) expect(inlineCompletion.session.selected).toEqual(item) }) it('should filter items based on range', async () => { const item1: InlineCompletionItem = { insertText: 'item1', range: Range.create(0, 0, 0, 1) } // Matches cursor at 0,0 const item2: InlineCompletionItem = { insertText: 'item2', range: Range.create(0, 1, 0, 2) } // Does not match cursor at 0,0 mockProvider.mockResolvedValue([item1, item2]) let doc = await workspace.document await inlineCompletion.trigger(doc.bufnr) expect(inlineCompletion.session).toBeDefined() expect(inlineCompletion.session.items).toEqual([item1]) }) it('should not trigger if document changed and autoTrigger is false without sync', async () => { const item: InlineCompletionItem = { insertText: 'suggested' } mockProvider.mockResolvedValue([item]) let doc = await workspace.document await nvim.call('setline', ['.', 'foobar']) expect(doc.hasChanged).toBe(true) const syncSpy = jest.spyOn(doc, 'synchronize') await inlineCompletion.trigger(doc.bufnr, { autoTrigger: false }) expect(syncSpy).toHaveBeenCalled() expect(inlineCompletion.session).toBeDefined() // Should still trigger after sync }) it('should not trigger if token is cancelled before provider call', async () => { mockProvider.mockResolvedValue([{ insertText: 'test' }]) let doc = await workspace.document const triggerPromise = inlineCompletion.trigger(doc.bufnr, {}, 10) // With delay await helper.doAction('inlineCancel') await triggerPromise expect(mockProvider).not.toHaveBeenCalled() expect(inlineCompletion.session).toBeUndefined() }) it('should not trigger if token is cancelled after provider call but before session creation', async () => { const item: InlineCompletionItem = { insertText: 'suggested' } mockProvider.mockImplementation(async () => { inlineCompletion.cancel() // Cancel while provider is "working" return [item] }) let doc = await workspace.document await inlineCompletion.trigger(doc.bufnr) expect(mockProvider).toHaveBeenCalled() expect(inlineCompletion.session).toBeUndefined() }) it('should not trigger if current state bufnr does not match', async () => { mockProvider.mockResolvedValue([{ insertText: 'test' }]) let prev = await helper.createDocument('foo') let doc = await helper.createDocument('bar') jest.spyOn(helper.plugin.handler, 'getCurrentState').mockResolvedValueOnce({ doc: prev, position: Position.create(0, 0), mode: 'i', winid: 1, } as any) await inlineCompletion.trigger(doc.bufnr) expect(mockProvider).not.toHaveBeenCalled() // Provider call is guarded by state check expect(inlineCompletion.session).toBeUndefined() }) it('should not trigger if current mode is not insert', async () => { mockProvider.mockResolvedValue([{ insertText: 'test' }]) let doc = await workspace.document jest.spyOn(helper.plugin.handler, 'getCurrentState').mockResolvedValueOnce({ doc: workspace.getDocument(doc.bufnr), position: Position.create(0, 0), mode: 'n', // Not insert mode winid: 1, } as any) await inlineCompletion.trigger(doc.bufnr) expect(mockProvider).not.toHaveBeenCalled() expect(inlineCompletion.session).toBeUndefined() }) it('should use specified provider if option.provider is given', async () => { const specificProviderMock = jest.fn().mockResolvedValue([{ insertText: 'specific' }]) const specificProviderDisposable = languages.registerInlineCompletionItemProvider( [{ language: '*' }], { provideInlineCompletionItems: specificProviderMock, __extensionName: 'mySpecificProvider' } as any, ) disposables.push(specificProviderDisposable) let doc = await workspace.document await inlineCompletion.trigger(doc.bufnr, { provider: 'mySpecificProvider' }) expect(specificProviderMock).toHaveBeenCalled() expect(mockProvider).not.toHaveBeenCalled() // Default provider should not be called expect(inlineCompletion.session).toBeDefined() expect(inlineCompletion.session.selected.insertText).toBe('specific') specificProviderDisposable.dispose() }) }) describe('next and prev', () => { const bufnr = 1 const item1: InlineCompletionItem = { insertText: 'item1' } const item2: InlineCompletionItem = { insertText: 'item2' } const item3: InlineCompletionItem = { insertText: 'item3' } let mockInsertVtext: jest.SpyInstance const setupSession = (items: InlineCompletionItem[], initialIndex = 0, sessionBufnr = bufnr) => { const session = new InlineSession(sessionBufnr, Position.create(0, 0), items) session.index = initialIndex inlineCompletion.session = session // Simulate that a previous insertVtext call set this if (items.length > 0 && session.selected) { // To make vtextBufnr match, we need to simulate a successful insertVtext inlineCompletion.session.vtext = session.selected.insertText as string } return session } beforeEach(() => { // Spy on insertVtext to check if it's called correctly without running its full logic mockInsertVtext = jest.spyOn(inlineCompletion, 'insertVtext').mockResolvedValue(undefined) // Ensure vtextBufnr is reset or managed correctly per test if (inlineCompletion.session) inlineCompletion.session.vtext = undefined }) afterEach(() => { mockInsertVtext.mockRestore() inlineCompletion.session = undefined }) describe('next()', () => { it('should do nothing if no session exists', async () => { inlineCompletion.session = undefined await inlineCompletion.next(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() }) it('should do nothing if bufnr does not match session vtextBufnr', async () => { setupSession([item1, item2]) inlineCompletion.session.vtext = undefined // Ensure vtextBufnr is -1 await inlineCompletion.next(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() setupSession([item1, item2], 0, bufnr) // vtextBufnr will be bufnr await inlineCompletion.next(bufnr + 1) // Call with different bufnr expect(mockInsertVtext).not.toHaveBeenCalled() }) it('should do nothing if session has no items', async () => { const session = setupSession([]) await inlineCompletion.next(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() expect(session.index).toBe(0) }) it('should do nothing if session has only one item', async () => { const session = setupSession([item1]) await inlineCompletion.next(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() expect(session.index).toBe(0) }) it('should move to the next item and call insertVtext', async () => { const session = setupSession([item1, item2, item3], 0) await inlineCompletion.next(bufnr) expect(session.index).toBe(1) expect(mockInsertVtext).toHaveBeenCalledWith(item2) }) it('should loop to the first item when at the last item', async () => { const session = setupSession([item1, item2, item3], 2) // Start at last item await helper.doAction('inlineNext', bufnr) expect(session.index).toBe(0) expect(mockInsertVtext).toHaveBeenCalledWith(item1) }) }) describe('prev()', () => { it('should do nothing if no session exists', async () => { inlineCompletion.session = undefined await inlineCompletion.prev(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() }) it('should do nothing if bufnr does not match session vtextBufnr', async () => { setupSession([item1, item2]) inlineCompletion.session.vtext = undefined // Ensure vtextBufnr is -1 await inlineCompletion.prev(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() setupSession([item1, item2], 0, bufnr) // vtextBufnr will be bufnr await inlineCompletion.prev(bufnr + 1) // Call with different bufnr expect(mockInsertVtext).not.toHaveBeenCalled() }) it('should do nothing if session has no items', async () => { const session = setupSession([]) await inlineCompletion.prev(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() expect(session.index).toBe(0) }) it('should do nothing if session has only one item', async () => { const session = setupSession([item1]) await inlineCompletion.prev(bufnr) expect(mockInsertVtext).not.toHaveBeenCalled() expect(session.index).toBe(0) }) it('should move to the previous item and call insertVtext', async () => { const session = setupSession([item1, item2, item3], 1) await helper.doAction('inlinePrev', bufnr) expect(session.index).toBe(0) expect(mockInsertVtext).toHaveBeenCalledWith(item1) }) it('should loop to the last item when at the first item', async () => { const session = setupSession([item1, item2, item3], 0) // Start at first item await inlineCompletion.prev(bufnr) expect(session.index).toBe(2) expect(mockInsertVtext).toHaveBeenCalledWith(item3) }) }) }) describe('commands', () => { describe('document.checkInlineCompletion', () => { let showWarningMessageSpy: jest.SpyInstance let showInformationMessageSpy: jest.SpyInstance let getDocumentSpy: jest.SpyInstance let getProvidersSpy: jest.SpyInstance beforeEach(() => { showWarningMessageSpy = jest.spyOn(window, 'showWarningMessage').mockResolvedValue(undefined) showInformationMessageSpy = jest.spyOn(window, 'showInformationMessage').mockResolvedValue(undefined) getDocumentSpy = jest.spyOn(workspace, 'getDocument') getProvidersSpy = jest.spyOn(languages.inlineCompletionItemManager, 'getProviders') }) afterEach(() => { jest.restoreAllMocks() }) it('should show warning if inline completion is not supported', async () => { jest.spyOn(workspace, 'has').mockReturnValue(false) await commands.executeCommand('document.checkInlineCompletion') expect(showWarningMessageSpy).toHaveBeenCalledWith(expect.stringContaining('Inline completion is not supported')) expect(showInformationMessageSpy).not.toHaveBeenCalled() }) it('should show warning if document is not found', async () => { getDocumentSpy.mockReturnValue(null) await commands.executeCommand('document.checkInlineCompletion') expect(showWarningMessageSpy).toHaveBeenCalledWith(expect.stringContaining(`not attached`)) expect(showInformationMessageSpy).not.toHaveBeenCalled() }) it('should show warning if document is not attached', async () => { const mockDoc = { bufnr: 1, attached: false, textDocument: {} } as any getDocumentSpy.mockReturnValue(mockDoc) await commands.executeCommand('document.checkInlineCompletion') expect(showWarningMessageSpy).toHaveBeenCalledWith(expect.stringContaining('not attached')) expect(showInformationMessageSpy).not.toHaveBeenCalled() }) it('should show warning when disabled by b:coc_inline_disable', async () => { let doc = await workspace.document await doc.buffer.setVar('coc_inline_disable', true) await commands.executeCommand('document.checkInlineCompletion') expect(showWarningMessageSpy).toHaveBeenCalledWith(expect.stringContaining('disabled')) expect(showInformationMessageSpy).not.toHaveBeenCalled() doc.buffer.deleteVar('coc_inline_disable') }) it('should show warning if no providers are found', async () => { const mockDoc = { bufnr: 1, attached: true, textDocument: {} } as any getDocumentSpy.mockReturnValue(mockDoc) getProvidersSpy.mockReturnValue([]) await commands.executeCommand('document.checkInlineCompletion') expect(showWarningMessageSpy).toHaveBeenCalledWith(expect.stringContaining('provider not found')) expect(showInformationMessageSpy).not.toHaveBeenCalled() }) it('should show information message if providers are found', async () => { const mockDoc = { bufnr: 1, attached: true, textDocument: {} } as any getDocumentSpy.mockReturnValue(mockDoc) const mockProvider1 = { provider: { __extensionName: 'providerOne' } } as any const mockProvider2 = { provider: {} } as any // No __extensionName getProvidersSpy.mockReturnValue([mockProvider1, mockProvider2]) await commands.executeCommand('document.checkInlineCompletion') expect(showInformationMessageSpy).toHaveBeenCalledWith('Inline completion is supported by providerOne, unknown.') expect(showWarningMessageSpy).not.toHaveBeenCalled() }) it('should show information message with single provider', async () => { const mockDoc = { bufnr: 1, attached: true, textDocument: {} } as any getDocumentSpy.mockReturnValue(mockDoc) const mockProvider = { provider: { __extensionName: 'myProvider' } } as any getProvidersSpy.mockReturnValue([mockProvider]) await commands.executeCommand('document.checkInlineCompletion') expect(showInformationMessageSpy).toHaveBeenCalledWith('Inline completion is supported by myProvider.') expect(showWarningMessageSpy).not.toHaveBeenCalled() }) }) }) }) // Tests for standalone functions describe('Utility functions', () => { describe('formatInsertText', () => { it('should format text with spaces', () => { const text = 'line1\n line2' const options: FormattingOptions = { tabSize: 2, insertSpaces: true } const result = formatInsertText(text, options) expect(result).toBe('line1\n line2') }) it('should convert tabs to spaces', () => { const text = 'line1\n\tline2' const options: FormattingOptions = { tabSize: 2, insertSpaces: true } const result = formatInsertText(text, options) expect(result).toBe('line1\n line2') }) it('should convert spaces to tabs', () => { const text = 'line1\n line2' const options: FormattingOptions = { tabSize: 2, insertSpaces: false } const result = formatInsertText(text, options) expect(result).toBe('line1\n\tline2') }) }) describe('getPumInserted', () => { it('should return empty string when current line matches synced line', async () => { const doc = await workspace.document await nvim.setLine('test line') await doc.patchChange() // Synchronize to ensure lines match const cursor = Position.create(0, 5) const result = getPumInserted(doc, cursor) expect(result).toBe('') }) it('should return inserted text when current line differs from synced line', async () => { const doc = await workspace.document // Set the line in the buffer but don't sync document await nvim.setLine('test inserted line') // Mock the textDocument.lines to simulate a synced state that's different const originalLines = doc.textDocument.lines doc.textDocument.lines = ['test line'] const cursor = Position.create(0, 13) // Position after "test inserted" const result = getPumInserted(doc, cursor) // Restore original lines doc.textDocument.lines = originalLines expect(result).toBe(' inserted') }) it('should return undefined when no valid insertion is detected', async () => { const doc = await workspace.document // Current line is completely different, not just an insertion await nvim.setLine('completely different') // Mock the textDocument.lines to simulate a synced state const originalLines = doc.textDocument.lines doc.textDocument.lines = ['original text'] const cursor = Position.create(0, 10) const result = getPumInserted(doc, cursor) // Restore original lines doc.textDocument.lines = originalLines expect(result).toBeUndefined() }) it('should handle cursor at beginning of line', async () => { const doc = await workspace.document await nvim.setLine('prefix original') const originalLines = doc.textDocument.lines doc.textDocument.lines = ['original'] const cursor = Position.create(0, 7) // Position after "prefix " const result = getPumInserted(doc, cursor) doc.textDocument.lines = originalLines expect(result).toBe('prefix ') }) it('should handle cursor at end of line', async () => { const doc = await workspace.document await nvim.setLine('original suffix') const originalLines = doc.textDocument.lines doc.textDocument.lines = ['original'] const cursor = Position.create(0, 15) // End of "original suffix" const result = getPumInserted(doc, cursor) doc.textDocument.lines = originalLines expect(result).toBe(' suffix') }) }) describe('getInsertText', () => { it('should handle plain text', () => { const item: InlineCompletionItem = { insertText: 'plain text' } const options: FormattingOptions = { tabSize: 2, insertSpaces: true } const result = getInsertText(item, options) expect(result).toBe('plain text') }) it('should handle snippet text', () => { const item: InlineCompletionItem = { insertText: { value: 'snippet ${1:text}', kind: 'snippet' }, } const options: FormattingOptions = { tabSize: 2, insertSpaces: true } const result = getInsertText(item, options) expect(result).toBe('snippet text') }) }) describe('getInserted', () => { it('should return undefined when current string is shorter than synced string', () => { const curr = 'foo' const synced = 'foobar' const character = 3 const result = getInserted(curr, synced, character) expect(result).toBeUndefined() }) it('should return undefined when text after cursor does not match end of synced string', () => { const curr = 'fooXYZ' const synced = 'foobar' const character = 3 const result = getInserted(curr, synced, character) expect(result).toBeUndefined() }) it('should return undefined when beginning of current does not match beginning of synced', () => { const curr = 'abcbar' const synced = 'foobar' const character = 3 const result = getInserted(curr, synced, character) expect(result).toBeUndefined() }) it('should identify simple insertion in the middle', () => { const curr = 'fooinsertedbartexthere' const synced = 'foobartexthere' const character = 11 // Position after "fooinserted" const result = getInserted(curr, synced, character) expect(result).toEqual({ start: 3, text: 'inserted' }) }) it('should identify insertion at the end', () => { const curr = 'foobarappended' const synced = 'foobar' const character = 14 // Position at the end of curr const result = getInserted(curr, synced, character) expect(result).toEqual({ start: 6, text: 'appended' }) }) it('should identify insertion at the beginning', () => { const curr = 'prefixfoobar' const synced = 'foobar' const character = 6 // Position after "prefix" const result = getInserted(curr, synced, character) expect(result).toEqual({ start: 0, text: 'prefix' }) }) it('should handle insertion with special characters', () => { const curr = 'foo\t\n🚀bar' const synced = 'foobar' const character = 7 // After special chars (note emoji is a single character) const result = getInserted(curr, synced, character) expect(result).toEqual({ start: 3, text: '\t\n🚀' }) }) it('should handle empty insertion', () => { const curr = 'foobar' const synced = 'foobar' const character = 3 // Position in the middle, but no change const result = getInserted(curr, synced, character) expect(result).toEqual({ start: 3, text: '' }) }) }) describe('checkInsertedAtBeginning', () => { it('should return true when item has no range and insertText starts with inserted string', () => { const currentLine = 'some text' const triggerCharacter = 4 const inserted = 'comp' const item: InlineCompletionItem = { insertText: 'completion' } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(true) }) it('should return false when item has no range and insertText does not start with inserted string', () => { const currentLine = 'some text' const triggerCharacter = 4 const inserted = 'diff' const item: InlineCompletionItem = { insertText: 'completion' } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(false) }) it('should return true when item has no range and snippet value starts with inserted string', () => { const currentLine = 'some text' const triggerCharacter = 4 const inserted = 'comp' const item: InlineCompletionItem = { insertText: { value: 'completion ${1:param}', kind: 'snippet' } } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(true) }) it('should return false when item has no range and snippet value does not start with inserted string', () => { const currentLine = 'some text' const triggerCharacter = 4 const inserted = 'diff' const item: InlineCompletionItem = { insertText: { value: 'completion ${1:param}', kind: 'snippet' } } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(false) }) it('should return true when item has range and current line portion matches start of insertText', () => { const currentLine = 'prefix completion suffix' const triggerCharacter = 10 // After "prefix com" const inserted = 'com' const item: InlineCompletionItem = { insertText: 'completion', range: Range.create(0, 7, 0, 16) // "completion" } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(true) }) it('should return false when item has range and current line portion does not match start of insertText', () => { const currentLine = 'prefix different suffix' const triggerCharacter = 10 // After "prefix dif" const inserted = 'dif' const item: InlineCompletionItem = { insertText: 'completion', range: Range.create(0, 7, 0, 16) // "different" } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(false) }) it('should return true when item has range and current line portion matches start of snippet value', () => { const currentLine = 'prefix completion suffix' const triggerCharacter = 10 // After "prefix com" const inserted = 'com' const item: InlineCompletionItem = { insertText: { value: 'completion ${1:param}', kind: 'snippet' }, range: Range.create(0, 7, 0, 16) // "completion" } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(true) }) it('should handle case with empty inserted string', () => { const currentLine = 'prefix' const triggerCharacter = 6 const inserted = '' const item: InlineCompletionItem = { insertText: 'completion' } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(true) // Empty string is always at beginning }) it('should handle special characters in inserted string', () => { const currentLine = 'prefix\t\n🚀completion' const triggerCharacter = 6 // After the emoji const inserted = '\t\n🚀' const item: InlineCompletionItem = { insertText: '\t\n🚀suffix', range: Range.create(0, 6, 0, 9) } const result = checkInsertedAtBeginning(currentLine, triggerCharacter, inserted, item) expect(result).toBe(true) }) }) }) ================================================ FILE: src/__tests__/handler/inlineCompletion.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CancellationTokenSource, Disposable, InlineCompletionContext, InlineCompletionItem, InlineCompletionTriggerKind } from 'vscode-languageserver-protocol' import languages from '../../languages' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) beforeEach(() => { }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) let items: InlineCompletionItem[] = [] function registerProvider(): void { disposables.push(languages.registerInlineCompletionItemProvider(['*'], { provideInlineCompletionItems: () => { return Promise.resolve(items) } })) } describe('InlineCompletion', () => { it('should provide completion items', async () => { let doc = await workspace.document let pos = await window.getCursorPosition() let context: InlineCompletionContext = { triggerKind: InlineCompletionTriggerKind.Automatic } let res = await languages.provideInlineCompletionItems(doc.textDocument, pos, context, CancellationToken.None) expect(res).toEqual([]) registerProvider() disposables.push(languages.registerInlineCompletionItemProvider(['*'], { provideInlineCompletionItems: () => { return Promise.resolve({ items: [InlineCompletionItem.create('foo')] }) } })) items = [InlineCompletionItem.create('bar')] res = await languages.provideInlineCompletionItems(doc.textDocument, pos, context, CancellationToken.None) expect(res.length).toBe(2) }) it('should return empty when token cancelled', async () => { let doc = await workspace.document let pos = await window.getCursorPosition() let context: InlineCompletionContext = { triggerKind: InlineCompletionTriggerKind.Automatic } let cancelled = false disposables.push(languages.registerInlineCompletionItemProvider(['*'], { provideInlineCompletionItems: (_doc, _pos, _context, token) => { return new Promise(resolve => { let timer = setTimeout(() => resolve([]), 500) token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) resolve(undefined) }) }) } })) let tokenSource = new CancellationTokenSource() let p = languages.provideInlineCompletionItems(doc.textDocument, pos, context, tokenSource.token) tokenSource.cancel() let res = await p expect(cancelled).toBe(true) expect(res).toEqual([]) }) it('should not throw on provider error', async () => { let doc = await workspace.document let pos = await window.getCursorPosition() let context: InlineCompletionContext = { triggerKind: InlineCompletionTriggerKind.Automatic } disposables.push(languages.registerInlineCompletionItemProvider(['*'], { provideInlineCompletionItems: () => { return Promise.reject(new Error('my error')) } })) let tokenSource = new CancellationTokenSource() let res = await languages.provideInlineCompletionItems(doc.textDocument, pos, context, tokenSource.token) expect(res).toEqual([]) }) }) ================================================ FILE: src/__tests__/handler/inlineValue.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, InlineValueText, Range } from 'vscode-languageserver-protocol' import languages, { ProviderName } from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim // hover = helper.plugin.getHandler().hover }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) describe('InlineValue', () => { describe('InlineValueManager', () => { it('should return false when provider not exists', async () => { let doc = await workspace.document let res = languages.hasProvider(ProviderName.InlineValue, doc.textDocument) expect(res).toBe(false) }) it('should return merged results', async () => { disposables.push(languages.registerInlineValuesProvider([{ language: '*' }], { provideInlineValues: () => { return null } })) disposables.push(languages.registerInlineValuesProvider([{ language: '*' }], { provideInlineValues: () => { return [ InlineValueText.create(Range.create(0, 0, 0, 1), 'foo'), InlineValueText.create(Range.create(0, 3, 0, 5), 'bar'), ] } })) disposables.push(languages.registerInlineValuesProvider([{ language: '*' }], { provideInlineValues: () => { return [ InlineValueText.create(Range.create(0, 0, 0, 1), 'foo'), ] } })) let doc = await workspace.document let res = await languages.provideInlineValues(doc.textDocument, Range.create(0, 0, 3, 0), { frameId: 3, stoppedLocation: Range.create(0, 0, 0, 3) }, CancellationToken.None) expect(res.length).toBe(2) }) }) }) ================================================ FILE: src/__tests__/handler/linkedEditing.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import LinkedEditingHandler from '../../handler/linkedEditing' import languages from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let handler: LinkedEditingHandler let disposables: Disposable[] = [] let wordPattern: string | undefined beforeAll(async () => { await helper.setup() nvim = helper.nvim handler = helper.plugin.getHandler().linkedEditingHandler }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { helper.updateConfiguration('coc.preferences.enableLinkedEditing', true) }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) async function registerProvider(content: string, position: Position): Promise { let doc = await workspace.document disposables.push(languages.registerLinkedEditingRangeProvider([{ language: '*' }], { provideLinkedEditingRanges: (doc, pos) => { let document = workspace.getDocument(doc.uri) let range = document.getWordRangeAtPosition(pos) if (!range) return null let text = doc.getText(range) let ranges: Range[] = document.getSymbolRanges(text) return { ranges, wordPattern } } })) await nvim.setLine(content) await doc.synchronize() await handler.enable(doc, position) } async function matches(): Promise { let list = await helper.getMatches('CocLinkedEditing') return list.length } describe('LinkedEditing', () => { it('should active and cancel on cursor moved', async () => { await registerProvider('foo foo a ', Position.create(0, 0)) expect(await matches()).toBe(2) await nvim.command(`normal! $`) await helper.waitValue(() => { return matches() }, 0) }) it('should active when moved to another word', async () => { await registerProvider('foo foo bar bar bar', Position.create(0, 0)) await nvim.call('cursor', [1, 9]) await helper.waitValue(() => { return matches() }, 3) }) it('should active on text change', async () => { let doc = await workspace.document await registerProvider('foo foo a ', Position.create(0, 0)) await nvim.call('cursor', [1, 1]) await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 0, ['i']]) await doc.synchronize() let line = await nvim.line expect(line).toBe('ifoo ifoo a ') await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 1, []]) await doc.synchronize() line = await nvim.line expect(line).toBe('foo foo a ') }) it('should cancel when change out of range', async () => { let doc = await workspace.document await registerProvider('foo foo bar', Position.create(0, 0)) await helper.waitValue(() => { return matches() }, 2) await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 9, 0, 10, ['']]) await doc.synchronize() await helper.waitValue(() => { return matches() }, 0) }) it('should not cancel when insert line break before range', async () => { let doc = await workspace.document await registerProvider('foo foo bar', Position.create(0, 0)) await helper.waitValue(() => { return matches() }, 2) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), '\n')]) await helper.waitValue(() => matches(), 2) }) it('should cancel when insert line break in range', async () => { let doc = await workspace.document await registerProvider('foo foo bar', Position.create(0, 0)) await helper.waitValue(() => { return matches() }, 2) await doc.applyEdits([TextEdit.insert(Position.create(0, 1), '\n ')]) await helper.waitValue(() => { return matches() }, 0) }) it('should cancel on editor change', async () => { await registerProvider('foo foo a ', Position.create(0, 0)) await nvim.command(`enew`) await helper.wait(50) await helper.waitValue(() => { return matches() }, 0) }) it('should cancel when insert none word character', async () => { await registerProvider('foo foo a ', Position.create(0, 0)) await nvim.call('cursor', [1, 4]) await nvim.input('i') await nvim.input('a') await helper.waitValue(() => { return matches() }, 2) await nvim.input('i') await nvim.input('@') await helper.waitValue(() => { return matches() }, 0) }) it('should cancel when insert not match wordPattern', async () => { wordPattern = '[A-Z]' await registerProvider('foo foo a ', Position.create(0, 0)) await nvim.call('cursor', [1, 4]) await nvim.input('i') await nvim.input('A') await helper.waitValue(() => { return matches() }, 2) await nvim.input('i') await nvim.input('3') await helper.waitValue(() => { return matches() }, 0) }) it('should cancel request on cursor moved', async () => { disposables.push(languages.registerLinkedEditingRangeProvider([{ language: '*' }], { provideLinkedEditingRanges: (doc, pos, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) resolve(null) }) let timer = setTimeout(() => { let document = workspace.getDocument(doc.uri) let range = document.getWordRangeAtPosition(pos) if (!range) return resolve(null) let text = doc.getText(range) let ranges: Range[] = document.getSymbolRanges(text) resolve({ ranges, wordPattern }) }, 1000) }) } })) let doc = await workspace.document await nvim.setLine('foo foo ') await doc.synchronize() await nvim.call('cursor', [1, 2]) await helper.wait(10) await nvim.call('cursor', [1, 9]) await helper.waitValue(() => { return matches() }, 0) }) }) ================================================ FILE: src/__tests__/handler/locations.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, CancellationTokenSource, Disposable, Location, LocationLink, Position, Range } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import LocationHandler from '../../handler/locations' import languages from '../../languages' import services from '../../services' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let locations: LocationHandler let disposables: Disposable[] = [] let currLocations: Location[] | LocationLink[] beforeAll(async () => { await helper.setup() nvim = helper.nvim Object.assign(workspace.env, { locationlist: false }) locations = helper.plugin.getHandler().locations }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) function createLocation(name: string, sl: number, sc: number, el: number, ec: number): Location { return Location.create(`test://${name}`, Range.create(sl, sc, el, ec)) } function createLocationLink(name: string, sl: number, sc: number, el: number, ec: number): LocationLink { let r = Range.create(sl, sc, el, ec) return LocationLink.create(`test://${name}`, r, r) } describe('locations', () => { describe('no provider', () => { it('should return null when provider does not exist', async () => { let doc = (await workspace.document).textDocument let pos = Position.create(0, 0) let tokenSource = new CancellationTokenSource() let token = tokenSource.token expect(await languages.getDefinition(doc, pos, token)).toEqual([]) expect(await languages.getDefinitionLinks(doc, pos, token)).toEqual([]) expect(await languages.getDeclaration(doc, pos, token)).toEqual([]) expect(await languages.getTypeDefinition(doc, pos, token)).toEqual([]) expect(await languages.getImplementation(doc, pos, token)).toEqual([]) expect(await languages.getReferences(doc, { includeDeclaration: false }, pos, token)).toEqual([]) }) }) describe('reference', () => { beforeEach(() => { disposables.push(languages.registerReferencesProvider([{ language: '*' }], { provideReferences: () => { return currLocations as any } })) }) it('should get references', async () => { currLocations = [createLocationLink('foo', 0, 0, 0, 0), createLocationLink('bar', 0, 0, 0, 0)] let res = await helper.doAction('references') expect(res.length).toBe(2) }) it('should jump to references', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0)] let res = await helper.doAction('jumpReferences', 'edit') expect(res).toBe(true) let name = await nvim.call('bufname', ['%']) expect(name).toBe('test://foo') }) it('should return false when references not found', async () => { currLocations = [] let res = await locations.gotoReferences('edit', true) expect(res).toBe(false) res = await helper.doAction('jumpUsed', 'edit') expect(res).toBe(false) }) }) describe('definition', () => { beforeEach(() => { disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return currLocations } })) }) it('should get definitions', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)] disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return [createLocation('foo', 0, 0, 0, 0)] } })) disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return createLocation('foo', 0, 0, 0, 0) } })) disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return [LocationLink.create(`test://foo`, Range.create(0, 0, 0, 0), Range.create(0, 0, 0, 0)), null] } })) disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return [LocationLink.create(`test://foo`, Range.create(0, 0, 0, 0), Range.create(0, 0, 0, 0))] } })) let res = await helper.doAction('definitions') expect(res.length).toBe(2) }) it('should return empty locations when no definitions exist', async () => { currLocations = null let doc = await workspace.document let res = await languages.getDefinitionLinks(doc.textDocument, Position.create(0, 0), CancellationToken.None) expect(res.length).toBe(0) currLocations = [createLocation('foo', 0, 0, 0, 0)] res = await languages.getDefinitionLinks(doc.textDocument, Position.create(0, 0), CancellationToken.None) expect(res.length).toBe(0) }) it('should jump to definitions', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0)] let res = await helper.doAction('jumpDefinition', 'edit') expect(res).toBe(true) let name = await nvim.call('bufname', ['%']) expect(name).toBe('test://foo') }) it('should return false when definitions not found', async () => { currLocations = [] let res = await locations.gotoDefinition('edit') expect(res).toBe(false) }) }) describe('declaration', () => { beforeEach(() => { disposables.push(languages.registerDeclarationProvider([{ language: '*' }], { provideDeclaration: () => { return currLocations } })) }) it('should get declarations', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)] let res = await locations.declarations() as Location[] expect(res.length).toBe(2) }) it('should jump to declaration', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0)] let res = await locations.gotoDeclaration('edit') expect(res).toBe(true) let name = await nvim.call('bufname', ['%']) expect(name).toBe('test://foo') }) it('should return false when declaration not found', async () => { currLocations = [] let res = await helper.doAction('jumpDeclaration', 'edit') expect(res).toBe(false) }) }) describe('typeDefinition', () => { beforeEach(() => { disposables.push(languages.registerTypeDefinitionProvider([{ language: '*' }], { provideTypeDefinition: () => { return currLocations } })) }) it('should get type definition', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)] let res = await helper.doAction('typeDefinitions') expect(res.length).toBe(2) }) it('should jump to type definition', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0)] let res = await locations.gotoTypeDefinition('edit') expect(res).toBe(true) let name = await nvim.call('bufname', ['%']) expect(name).toBe('test://foo') }) it('should return false when type definition not found', async () => { currLocations = [] let res = await helper.doAction('jumpTypeDefinition', 'edit') expect(res).toBe(false) }) }) describe('implementation', () => { beforeEach(() => { disposables.push(languages.registerImplementationProvider([{ language: '*' }], { provideImplementation: () => { return currLocations } })) }) it('should get implementations', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)] let res = await helper.doAction('implementations') expect(res.length).toBe(2) }) it('should jump to implementation', async () => { currLocations = [createLocation('foo', 0, 0, 0, 0)] let res = await helper.doAction('jumpImplementation', 'edit') expect(res).toBe(true) let name = await nvim.call('bufname', ['%']) expect(name).toBe('test://foo') }) it('should return false when implementation not found', async () => { currLocations = [] let res = await locations.gotoImplementation('edit') expect(res).toBe(false) }) }) describe('getTagList', () => { it('should return null when cword does not exist', async () => { let res = await helper.doAction('getTagList') expect(res).toBe(null) }) it('should return null when provider does not exist', async () => { await nvim.setLine('foo') await nvim.command('normal! ^') let res = await locations.getTagList() expect(res).toBe(null) }) it('should null when buffer not attached', async () => { let doc = await workspace.document if (doc) doc.detach() let res = await locations.getTagList() expect(res).toBe(null) }) it('should return null when result is empty', async () => { disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return [] } })) await nvim.setLine('foo') await nvim.command('normal! ^') let res = await locations.getTagList() expect(res).toBe(null) }) it('should return tag definitions', async () => { disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { provideDefinition: () => { return [createLocation('bar', 2, 0, 2, 5), Location.create(URI.file('/foo').toString(), Range.create(1, 0, 1, 5))] } })) await nvim.setLine('foo') await nvim.command('normal! ^') let res = await locations.getTagList() expect(res).toEqual([ { name: 'foo', cmd: 'silent keepjumps call coc#cursor#move_to(2, 0)', filename: 'test://bar' }, { name: 'foo', cmd: 'silent keepjumps call coc#cursor#move_to(1, 0)', filename: '/foo' } ]) }) }) describe('findLocations', () => { // hook result let fn let result: any beforeAll(() => { fn = services.sendRequest services.sendRequest = () => { return Promise.resolve(result) } }) afterAll(() => { services.sendRequest = fn }) it('should handle locations from language client', async () => { result = [createLocation('bar', 2, 0, 2, 5)] await helper.doAction('findLocations', 'foo', 'mylocation', {}, false) let res = await nvim.getVar('coc_jump_locations') expect(res).toEqual([{ uri: 'test://bar', lnum: 3, end_lnum: 3, col: 1, end_col: 6, filename: 'test://bar', text: '', range: Range.create(2, 0, 2, 5) }]) }) it('should handle empty result', async () => { result = null let res = await locations.findLocations('foo', 'mylocation', undefined, 'edit') expect(res).toBe(false) }) it('should handle nested locations', async () => { let location: any = { location: createLocation('file', 0, 0, 0, 0), children: [{ location: createLocation('foo', 3, 0, 3, 5), children: [] }, { location: createLocation('bar', 4, 0, 4, 5), children: [] }] } result = location await locations.findLocations('foo', 'mylocation', {}) let res = await nvim.getVar('coc_jump_locations') as any[] expect(res.length).toBe(3) }) }) describe('toLocations()', () => { it('should convert to locations', async () => { let loc = createLocation('file', 0, 0, 0, 0) expect(locations.toLocations(loc).length).toBe(1) expect(locations.toLocations([loc]).length).toBe(1) let link = LocationLink.create(`test://a`, Range.create(0, 0, 1, 0), Range.create(0, 0, 0, 1)) expect(locations.toLocations(link).length).toBe(1) expect(locations.toLocations([link]).length).toBe(1) expect(locations.toLocations(null).length).toBe(0) expect(locations.toLocations(undefined).length).toBe(0) let location: any = { location: createLocation('file', 0, 0, 0, 0), children: [{ location: link, children: [{ location: loc }, null, undefined, {}] }] } expect(locations.toLocations(location).length).toBe(3) }) }) describe('handleLocations', () => { it('should not throw when locations is undefined', async () => { await locations.handleLocations(undefined) }) it('should not throw when locations is empty array', async () => { await locations.handleLocations([]) }) }) }) ================================================ FILE: src/__tests__/handler/outline.test.ts ================================================ import { Buffer, Neovim } from '@chemzqm/neovim' import { CodeAction, CodeActionKind, Disposable, DocumentSymbol, Range, SymbolKind, SymbolTag, TextEdit } from 'vscode-languageserver-protocol' import events from '../../events' import Symbols from '../../handler/symbols/index' import languages from '../../languages' import { ProviderResult } from '../../provider' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' import Parser from './parser' let nvim: Neovim let symbols: Symbols let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim symbols = helper.plugin.getHandler().symbols }) beforeEach(() => { disposables.push(languages.registerDocumentSymbolProvider([{ language: 'javascript' }], { provideDocumentSymbols: document => { let content = document.getText() let showDetail = content.includes('detail') let parser = new Parser(content, showDetail) let res: DocumentSymbol[] = parser.parse() if (res.length) { res[0].tags = [SymbolTag.Deprecated] } return Promise.resolve(res) } })) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() await nvim.command(`let w:cocViewId = ''`) }) async function getOutlineBuffer(): Promise { let winid = await nvim.call('coc#window#find', ['cocViewId', 'OUTLINE']) if (winid == -1) return undefined let bufnr = await nvim.call('winbufnr', [winid]) as number if (bufnr == -1) return undefined return nvim.createBuffer(bufnr) } describe('symbols outline', () => { let defaultCode = `class myClass { fun1() { } fun2() {} }` async function createBuffer(code = defaultCode): Promise { let doc = await helper.createDocument() let buf = doc.buffer doc.setFiletype('javascript') await buf.setOption('modifiable', true) await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() return buf } describe('actions', () => { it('should invoke selected code action', async () => { const codeAction = CodeAction.create('my action', CodeActionKind.Refactor) let uri: string disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { provideCodeActions: () => [codeAction], resolveCodeAction: (action): ProviderResult => { action.edit = { changes: { [uri]: [TextEdit.del(Range.create(0, 0, 0, 5))] } } return action } }, undefined)) await createBuffer() let bufnr = await nvim.call('bufnr', ['%']) as number let doc = workspace.getDocument(bufnr) uri = doc.uri await symbols.showOutline(0) await helper.waitValue(async () => { let id = await nvim.eval('get(w:,"cocViewId",v:null)') return id != null }, true) await nvim.call('cursor', [3, 1]) let spy = jest.spyOn(window, 'showMenuPicker').mockImplementation(() => { return Promise.resolve(0) }) await nvim.input('') await helper.waitValue(async () => { return await nvim.eval('getline(1)') }, ' myClass {') spy.mockRestore() }) it('should invoke visual select', async () => { await createBuffer() let bufnr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) await helper.waitFor('getline', [3], /fun1/) await nvim.command('exe 3') await nvim.input('') await helper.waitPrompt() await nvim.input('') await helper.waitFor('mode', [], 'v') let buf = await nvim.buffer expect(buf.id).toBe(bufnr) }) }) describe('configuration', () => { it('should follow cursor', async () => { await createBuffer(` class myClass { fun1() { } fun2() {} }`) let curr = await nvim.call('bufnr', ['%']) as number await symbols.showOutline(0) let bufnr = await nvim.call('bufnr', ['%']) as number await nvim.command('wincmd p') await nvim.command('exe 3') await events.fire('CursorHold', [curr, [3, 1]]) await helper.wait(30) await nvim.call('cursor', [1, 1]) await events.fire('CursorHold', [curr, [1, 1]]) await helper.wait(30) let buf = nvim.createBuffer(bufnr) let lines = await buf.getLines() expect(lines.slice(1)).toEqual([ '- c myClass 1', ' m fun1 2', ' m fun2 3' ]) let signs = await buf.getSigns({ group: 'CocTree' }) expect(signs.length).toBe(1) expect(signs[0]).toEqual({ lnum: 2, id: 3001, name: 'CocTreeSelected', priority: 10, group: 'CocTree' }) await nvim.command(`bd ${bufnr}`) await events.fire('CursorHold', [curr, [3, 1]]) }) it('should not follow cursor', async () => { helper.updateConfiguration('outline.followCursor', false, disposables) await createBuffer() let curr = await nvim.call('bufnr', ['%']) as number await symbols.showOutline(0) let bufnr = await nvim.call('bufnr', ['%']) as number await nvim.command('wincmd p') await nvim.command('exe 3') await events.fire('CursorHold', [curr]) await helper.wait(50) let buf = nvim.createBuffer(bufnr) let signs = await buf.getSigns({ group: 'CocTree' }) expect(signs.length).toBe(0) }) it('should keep current window', async () => { helper.updateConfiguration('outline.keepWindow', true, disposables) await createBuffer() let curr = await nvim.call('bufnr', ['%']) await symbols.showOutline() let bufnr = await nvim.call('bufnr', ['%']) expect(curr).toBe(bufnr) }) it('should check on buffer switch', async () => { helper.updateConfiguration('outline.checkBufferSwitch', true, disposables) let b = await createBuffer() await symbols.showOutline(1) let buf = await getOutlineBuffer() let bufnr = buf.id await helper.edit('unnamed') await helper.waitValue(async () => { let buf = await getOutlineBuffer() return buf.id > bufnr }, true) buf = await getOutlineBuffer() let lines = await buf.lines expect(lines[0]).toMatch('Document symbol provider not found') await nvim.command(`bd! ${b.id}`) await helper.wait(10) let loaded = await buf.loaded expect(loaded).toBe(true) }) it('should not check on buffer switch', async () => { helper.updateConfiguration('outline.checkBufferSwitch', false, disposables) await createBuffer() await symbols.showOutline(1) await helper.edit('unnamed') await helper.wait(100) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines.slice(1)).toEqual([ '- c myClass 1', ' m fun1 2', ' m fun2 3' ]) }) it('should not check on buffer reload', async () => { helper.updateConfiguration('outline.checkBufferSwitch', false, disposables) await symbols.showOutline(1) await createBuffer() await helper.wait(50) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should sort by category', async () => { let code = ` class myClass { } fun1() {} ` await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines).toEqual([ 'OUTLINE Category', ' c myClass 2', ' m fun1 4' ]) }) it('should sort by position', async () => { let code = `class myClass { fun2() { } fun1() {} }` helper.updateConfiguration('outline.sortBy', 'position', disposables) await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines).toEqual([ 'OUTLINE Position', '- c myClass 1', ' m fun2 2', ' m fun1 3' ]) }) it('should sort by name', async () => { let code = `class myClass { fun2() {} fun1() {} }` helper.updateConfiguration('outline.sortBy', 'name', disposables) await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines).toEqual([ 'OUTLINE Name', '- c myClass 1', ' m fun1 3', ' m fun2 2' ]) }) it('should change sort method', async () => { helper.updateConfiguration('outline.detailAsDescription', false, disposables) let code = `class detail { fun2() {} fun1() {} }` await createBuffer(code) await symbols.showOutline(0) await helper.wait(30) await nvim.input('') await helper.waitPrompt() await nvim.input('') await helper.wait(30) await nvim.input('') await helper.waitPrompt() await nvim.input('3') await helper.waitFor('getline', [1], 'OUTLINE Position') }) it('should show detail as description', async () => { helper.updateConfiguration('outline.detailAsDescription', true, disposables) let code = `class detail { fun2() {} }` await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines.slice(1)).toEqual([ '- c detail 1', ' m fun2 () 2' ]) }) it('should not showLineNumber', async () => { helper.updateConfiguration('outline.showLineNumber', false, disposables) let code = `class detail { fun2() {} }` await createBuffer(code) await symbols.showOutline(1) let buf = await getOutlineBuffer() let lines = await buf.lines expect(lines.slice(1)).toEqual(['- c detail', ' m fun2 ()']) }) }) describe('events', () => { it('should not close TreeView on buffer reload', async () => { await createBuffer() await symbols.showOutline(0) await nvim.command('edit') await helper.wait(30) let winid = await nvim.call('coc#window#find', ['cocViewId', 'OUTLINE']) expect(winid).toBeGreaterThan(0) }) it('should dispose on buffer unload', async () => { await createBuffer() let curr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) await nvim.command('tabe') await nvim.command(`bd! ${curr}`) await helper.waitValue(async () => { let buf = await getOutlineBuffer() return buf == null }, true) }) it('should check current window on BufEnter', async () => { await createBuffer() await symbols.showOutline(1) await nvim.command('enew') await helper.wait(50) }) it('should recreated when original window exists', async () => { let win = await nvim.window await symbols.showOutline(1) await helper.wait(50) await nvim.setWindow(win) await createBuffer() await helper.waitValue(async () => { let buf = await getOutlineBuffer() return buf != null }, true) }) it('should keep old outline when new buffer not attached', async () => { await createBuffer() await symbols.showOutline(1) await nvim.command(`vnew +setl\\ buftype=nofile`) await helper.wait(50) let buf = await getOutlineBuffer() expect(buf).toBeDefined() let lines = await buf.lines expect(lines.slice(1)).toEqual([ '- c myClass 1', ' m fun1 2', ' m fun2 3' ]) }) it('should not reload when switch to original buffer', async () => { await createBuffer() await symbols.showOutline(0) let buf = await getOutlineBuffer() let name = await buf.name await nvim.command('wincmd p') await helper.wait(50) buf = await getOutlineBuffer() let curr = await buf.name expect(curr).toBe(name) }) }) describe('show()', () => { it('should not throw when document not attached', async () => { await nvim.command(`edit +setl\\ buftype=nofile t`) await workspace.document await symbols.showOutline(1) let line = await helper.getCmdline() expect(line).toMatch('Unable to show outline') }) it('should not throw when provider does not exist', async () => { await symbols.showOutline(1) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should not throw when symbols is empty', async () => { await createBuffer('') await symbols.showOutline(1) let buf = await getOutlineBuffer() expect(buf).toBeDefined() }) it('should jump to selected symbol', async () => { await createBuffer() let bufnr = await nvim.call('bufnr', ['%']) await symbols.showOutline(0) await helper.waitFor('getline', [3], ' m fun1 2') await nvim.command('exe 3') await nvim.input('') await helper.waitValue(async () => { return await nvim.call('bufnr', ['%']) }, bufnr) let cursor = await nvim.call('coc#cursor#position') expect(cursor).toEqual([1, 2]) }) it('should update symbols', async () => { await createBuffer() let doc = await workspace.document let bufnr = await nvim.call('bufnr', ['%']) as number await symbols.showOutline(1) await helper.waitFor('getline', [1], 'class myClass {') let buf = nvim.createBuffer(bufnr) let code = 'class foo{}' await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() buf = await getOutlineBuffer() await helper.waitFor('eval', [`getbufline(${buf.id},1)[0]`], /No\sresults/) let lines = await buf.lines expect(lines).toEqual([ 'No results', '', 'OUTLINE Category' ]) }) it('should show label in description', async () => { disposables.push(languages.registerDocumentSymbolProvider([{ language: 'vim' }], { meta: { label: 'vimlsp' }, provideDocumentSymbols: _ => { let res: DocumentSymbol[] = [{ name: 'let', range: Range.create(0, 0, 0, 3), kind: SymbolKind.Constant, selectionRange: Range.create(0, 0, 0, 3), tags: [SymbolTag.Deprecated] }] return Promise.resolve(res) } })) let doc = await helper.createDocument('t.vim') doc.setFiletype('vim') let buf = await nvim.buffer await buf.setLines(['let'], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() await symbols.showOutline(0) await helper.waitFor('getline', [1], 'OUTLINE vimlsp') }) }) describe('autoPreview', () => { it('should toggle auto preview by press p', async () => { await createBuffer() await symbols.showOutline(0) await helper.waitFor('getline', [3], /fun1/) await nvim.command('exe 2') await nvim.input('p') let winid = await helper.waitFloat() expect(winid).toBeGreaterThan(1000) await nvim.input('p') await helper.waitValue(async () => { let win = nvim.createWindow(winid) let valid = await win.valid return valid === false }, true) }) it('should close preview when move to line without node', async () => { await createBuffer() await symbols.showOutline(0) await helper.waitFor('getline', [3], /fun1/) await nvim.command('exe 2') await nvim.input('p') let winid = await helper.waitFloat() await nvim.input('l') // debounce for CursorMoved used await helper.wait(50) await nvim.input('k') await helper.waitValue(async () => { let win = nvim.createWindow(winid) let valid = await win.valid return valid === false }, true) }) it('should show preview when move cursor back', async () => { await createBuffer() await symbols.showOutline(0) await helper.waitFor('getline', [3], /fun1/) await nvim.command('exe 2') await nvim.input('p') let winid = await helper.waitFloat() await nvim.command('wincmd p') await helper.waitValue(async () => { let win = nvim.createWindow(winid) let valid = await win.valid return valid === false }, true) await nvim.command('wincmd p') winid = await helper.waitFloat() expect(winid).toBeGreaterThan(1000) }) it('should enable auto preview by configuration', async () => { helper.updateConfiguration('outline.autoPreview', true, disposables) await createBuffer() await symbols.showOutline(0) await helper.waitFor('getline', [3], /fun1/) await nvim.command('exe 2') let winid = await helper.waitFloat() expect(winid).toBeGreaterThan(1000) }) }) describe('hide()', () => { it('should hide outline', async () => { await createBuffer('') await helper.doAction('showOutline', 1) await helper.doAction('hideOutline') let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) it('should auto hide outline on clicking', async () => { helper.updateConfiguration('outline.autoHide', true, disposables) await createBuffer() await symbols.showOutline() await helper.waitFor('getline', [3], ' m fun1 2') await nvim.command('exe 3') await nvim.input('') await helper.waitValue(async () => { return await getOutlineBuffer() }, undefined) }) it('should not throw when outline does not exist', async () => { await symbols.hideOutline() let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) }) describe('dispose', () => { it('should dispose provider and views', async () => { await createBuffer('') let bufnr = await nvim.call('bufnr', ['%']) as number await symbols.showOutline(1) symbols.dispose() await helper.waitValue(() => { return symbols.hasOutline(bufnr) }, false) let buf = await getOutlineBuffer() expect(buf).toBeUndefined() }) }) }) ================================================ FILE: src/__tests__/handler/parser.ts ================================================ import { DocumentSymbol, Range, SymbolKind } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' /** * A syntax parser that parse `class` and `method` only. */ export default class Parser { private _curr = 0 private _symbols: DocumentSymbol[] = [] private currSymbol: DocumentSymbol | undefined private len: number private textDocument: TextDocument constructor(private _content: string, private showDetail = false) { this.len = _content.length this.textDocument = TextDocument.create('test:///a', 'txt', 1, _content) } public parse(): DocumentSymbol[] { while (this._curr <= this.len - 1) { this.parseToken() } return this._symbols } /** * Parse a symbol, reset currSymbol & _curr */ private parseToken(): void { this.skipSpaces() if (this.currSymbol) { let endOffset = this.textDocument.offsetAt(this.currSymbol.range.end) if (this._curr > endOffset) { this.currSymbol = undefined } } let remain = this.getLineRemain() let ms = remain.match(/^(class)\s(\w+)\s\{\s*/) if (ms) { // find class let start = this._curr + 6 let end = start + ms[2].length let selectionRange = Range.create(this.textDocument.positionAt(start), this.textDocument.positionAt(end)) let endPosition = this.findMatchedIndex(this._curr + ms[0].length) let range = Range.create(this.textDocument.positionAt(this._curr), this.textDocument.positionAt(endPosition)) let symbolInfo: DocumentSymbol = { range, selectionRange, kind: SymbolKind.Class, name: ms[2], children: [] } if (this.currSymbol && this.currSymbol.children) { this.currSymbol.children.push(symbolInfo) } else { this._symbols.push(symbolInfo) } this.currSymbol = symbolInfo } else { let ms = remain.match(/(\w+)\((.*)\)\s*\{/) if (ms) { // find method let start = this._curr let end = start + ms[1].length let selectionRange = Range.create(this.textDocument.positionAt(start), this.textDocument.positionAt(end)) let endPosition = this.findMatchedIndex(this._curr + ms[0].length) let range = Range.create(this.textDocument.positionAt(this._curr), this.textDocument.positionAt(endPosition)) let symbolInfo: DocumentSymbol = { range, selectionRange, kind: SymbolKind.Method, detail: this.showDetail ? `(${ms[2]})` : undefined, name: ms[1] } if (this.currSymbol && this.currSymbol.children) { this.currSymbol.children.push(symbolInfo) } else { this._symbols.push(symbolInfo) } } } this._curr = this._curr + remain.length + 1 } private findMatchedIndex(start: number): number { let level = 0 for (let i = start; i < this.len; i++) { let ch = this._content[i] if (ch == '{') { level = level + 1 } if (ch == '}') { if (level == 0) return i level = level - 1 } } throw new Error(`Can't find matched }`) } private getLineRemain(): string { let chars = '' for (let i = this._curr; i < this.len; i++) { let ch = this._content[i] if (ch == '\n') break chars = chars + ch } return chars } private skipSpaces(): void { for (let i = this._curr; i < this.len; i++) { let ch = this._content[i] if (!ch || /\S/.test(ch)) { this._curr = i break } } } } ================================================ FILE: src/__tests__/handler/refactor.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import { Position, Range, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../../commands' import RefactorBuffer, { FileItemDef, fixChangeParams } from '../../handler/refactor/buffer' import Changes from '../../handler/refactor/changes' import Refactor from '../../handler/refactor/index' import languages from '../../languages' import { DidChangeTextDocumentParams } from '../../types' import { Disposable } from '../../util' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let refactor: Refactor beforeAll(async () => { await helper.setup() nvim = helper.nvim refactor = helper.plugin.getHandler().refactor }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { refactor.reset() await helper.reset() }) function createEdit(uri: string): WorkspaceEdit { let edit = TextEdit.insert(Position.create(0, 0), 'a') let doc = { uri, version: null } return { documentChanges: [TextDocumentEdit.create(doc, [edit])] } } // assert ranges is expected. async function assertSynchronized(buf: RefactorBuffer) { let buffer = nvim.createBuffer(buf.bufnr) let lines = await buffer.lines let items: { lnum: number, lines: string[] }[] = [] for (let i = 0; i < lines.length; i++) { let line = lines[i] if (line.includes('\u3000') && line.length > 1) { items.push({ lnum: i + 1, lines: [] }) } } let curr: { lnum: number, lines: string[] }[] = [] buf.fileItems.forEach(item => { item.ranges.forEach(r => { curr.push({ lnum: r.lnum, lines: [] }) }) }) curr.sort((a, b) => a.lnum - b.lnum) expect(items).toEqual(curr) } describe('fixChangeParams', () => { function createChangeParams(range: Range, text: string, original: string, originalLines: ReadonlyArray): DidChangeTextDocumentParams { return { textDocument: { uri: 'untitled:/1', version: 1, }, originalLines, original, bufnr: 1, contentChanges: [{ range, text }] } as any } it('should fix delete change params', async () => { let e = createChangeParams(Range.create(0, 4, 2, 4), '', 'x\nfoo\n\u3000bar', [ '\u3000barx', 'foo', '\u3000bara' ]) e = fixChangeParams(e) expect(e.original).toBe('\u3000barx\nfoo\n') expect(e.contentChanges[0].range).toEqual(Range.create(0, 0, 2, 0)) }) it('should fix insert change params', async () => { let e = createChangeParams(Range.create(0, 4, 0, 4), 'x\nfoo\n\u3000bar', '', [ '\u3000bara' ]) e = fixChangeParams(e) expect(e.original).toBe('') let change = e.contentChanges[0] expect(change.range).toEqual(Range.create(0, 0, 0, 0)) expect(change.text).toBe('\u3000barx\nfoo\n') }) }) describe('refactor', () => { describe('checkInsert()', () => { it('should check inserted ranges', async () => { let c = new Changes() expect(c.checkInsert([1])).toBeUndefined() c.add([{ filepath: __filename, start: 1, lnum: 1, lines: [''] }]) expect(c.checkInsert([2])).toBeUndefined() }) }) describe('getFileRange()', () => { it('should throw when range does not exist', async () => { let uri = URI.file(__filename).toString() let locations = [{ uri, range: Range.create(0, 0, 0, 6) }] let buf = await refactor.fromLocations(locations) let fn = () => { buf.getFileRange(1) } expect(fn).toThrow(Error) }) it('should find file range', async () => { let uri = URI.file(__filename).toString() let locations = [{ uri, range: Range.create(0, 0, 0, 6) }] let buf = await commands.executeCommand('editor.action.showRefactor', locations) let res = buf.getFileRange(4) expect(res).toBeDefined() }) }) describe('getRange()', () => { it('should get delete range', async () => { let filename = await createTmpFile('foo\n\nbar\n') let fileItem: FileItemDef = { filepath: filename, ranges: [{ start: 0, end: 1 }, { start: 2, end: 3 }] } let buf = await refactor.createRefactorBuffer() await buf.addFileItems([fileItem]) let res = buf.getFileRange(4) let r = buf.getDeleteRange(res) expect(r).toEqual(Range.create(3, 0, 6, 0)) res = buf.getFileRange(7) r = buf.getDeleteRange(res) expect(r).toEqual(Range.create(6, 0, 8, 0)) }) it('should get replace range', async () => { let filename = await createTmpFile('foo\n\nbar\n') let fileItem: FileItemDef = { filepath: filename, ranges: [{ start: 0, end: 1 }, { start: 2, end: 3 }] } let buf = await refactor.createRefactorBuffer() await buf.addFileItems([fileItem]) let res = buf.getFileRange(4) let r = buf.getReplaceRange(res) expect(r).toEqual(Range.create(4, 0, 4, 3)) res = buf.getFileRange(7) r = buf.getReplaceRange(res) expect(r).toEqual(Range.create(7, 0, 7, 3)) }) }) describe('fromWorkspaceEdit()', () => { it('should not create from invalid workspaceEdit', async () => { let res = await refactor.fromWorkspaceEdit(undefined) expect(res).toBeUndefined() res = await refactor.fromWorkspaceEdit({ documentChanges: [] }) expect(res).toBeUndefined() }) it('should create from document changes', async () => { let edit = createEdit(URI.file(__filename).toString()) let buf = await refactor.fromWorkspaceEdit(edit) let shown = await buf.valid expect(shown).toBe(true) let items = buf.fileItems expect(items.length).toBe(1) await nvim.command(`bd! ${buf.bufnr}`) await helper.wait(30) let has = refactor.has(buf.bufnr) expect(has).toBe(false) }) it('should create from workspaceEdit', async () => { let changes = { [URI.file(__filename).toString()]: [{ range: Range.create(0, 0, 0, 6), newText: '' }, { range: Range.create(1, 0, 1, 6), newText: '' }, { range: Range.create(50, 0, 50, 1), newText: ' ' }, { range: Range.create(60, 0, 60, 1), newText: ' ' }] } let edit: WorkspaceEdit = { changes } let buf = await refactor.fromWorkspaceEdit(edit) let shown = await buf.valid expect(shown).toBe(true) let items = buf.fileItems expect(items.length).toBe(1) }) }) describe('fromLocations()', () => { it('should create from locations', async () => { let uri = URI.file(__filename).toString() let locations = [{ uri, range: Range.create(0, 0, 0, 6), }, { uri, range: Range.create(1, 0, 1, 6), }] let buf = await refactor.fromLocations(locations) let shown = await buf.valid expect(shown).toBe(true) let items = buf.fileItems expect(items.length).toBe(1) }) it('should not create from empty locations', async () => { let buf = await refactor.fromLocations([]) expect(buf).toBeUndefined() }) }) describe('onChange()', () => { async function setup(): Promise { let uri = URI.file(__filename).toString() let locations = [{ uri, range: Range.create(0, 0, 0, 6), }, { uri, range: Range.create(1, 0, 1, 6), }, { uri, range: Range.create(10, 0, 10, 6), }] return await refactor.fromLocations(locations) } it('should refresh on empty text change', async () => { let buf = await setup() let line = await nvim.call('getline', [4]) let doc = workspace.getDocument(buf.bufnr) await nvim.call('setline', [4, line]) doc._forceSync() let srcId = await nvim.createNamespace('coc-refactor') let markers = await doc.buffer.getExtMarks(srcId, 0, -1) expect(markers.length).toBe(2) }) it('should detect range delete and undo', async () => { let buf = await setup() let doc = workspace.getDocument(buf.bufnr) let r = buf.getFileRange(4) let end = r.lnum + r.lines.length await nvim.command(`${r.lnum},${end + 1}d`) await doc.synchronize() await assertSynchronized(buf) await nvim.command('undo') await doc.synchronize() await assertSynchronized(buf) }) it('should detect normal delete', async () => { let buf = await setup() let doc = workspace.getDocument(buf.bufnr) let r = buf.getFileRange(4) await nvim.command(`${r.lnum + 1},${r.lnum + 1}d`) await doc.synchronize() await assertSynchronized(buf) }) it('should detect insert', async () => { let buf = await setup() let doc = workspace.getDocument(buf.bufnr) let buffer = nvim.createBuffer(buf.bufnr) await buffer.append(['foo']) await doc.synchronize() await assertSynchronized(buf) await buffer.append(['foo', '\u3000']) await doc.synchronize() await assertSynchronized(buf) }) }) describe('onDocumentChange()', () => { it('should ignore when change after range', async () => { let doc = await helper.createDocument() await doc.buffer.append(['foo', 'bar']) await doc.synchronize() let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(0, 0, 0, 3) }]) let lines = await nvim.call('getline', [1, '$']) await doc.buffer.append(['def']) await doc.synchronize() let newLines = await nvim.call('getline', [1, '$']) expect(lines).toEqual(newLines) await assertSynchronized(buf) }) it('should adjust when change before range', async () => { let doc = await helper.createDocument() await doc.buffer.append(['', '', '', '', 'foo', 'bar']) await doc.synchronize() let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(4, 0, 4, 3) }]) await doc.buffer.setLines(['def'], { start: 0, end: 0, strictIndexing: false }) await doc.synchronize() let fileRange = buf.getFileRange(4) expect(fileRange.start).toBe(2) expect(fileRange.lines.length).toBe(6) await assertSynchronized(buf) }) it('should remove ranges when lines empty', async () => { let doc = await helper.createDocument() await doc.buffer.append(['', '', '', '', 'foo', 'bar']) await doc.synchronize() let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(4, 0, 4, 3) }]) await doc.buffer.setLines([], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() let lines = await nvim.call('getline', [1, '$']) as string[] expect(lines.length).toBe(3) let items = buf.fileItems expect(items.length).toBe(0) await assertSynchronized(buf) }) it('should change when liens changed', async () => { let doc = await helper.createDocument() await doc.buffer.append(['', '', '', '', 'foo', 'bar']) await doc.synchronize() let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(4, 0, 4, 3) }]) await doc.buffer.setLines(['def', 'def'], { start: 5, end: 6, strictIndexing: false }) await doc.synchronize() let lines = await nvim.call('getline', [1, '$']) as string[] expect(lines[lines.length - 2]).toBe('def') await assertSynchronized(buf) }) }) describe('getFileChanges()', () => { it('should get changes #1', async () => { await helper.createDocument() let lines = ` Save current buffer to make changes \u3000 \u3000 \u3000/a.ts }) } ` let buf = await refactor.fromLines(lines.split('\n')) let changes = await buf.getFileChanges() expect(changes).toEqual([{ lnum: 5, filepath: '/a.ts', lines: [' })', ' } '] }]) }) it('should get changes #2', async () => { let lines = ` \u3000/a.ts }) } ` let buf = await refactor.fromLines(lines.split('\n')) let changes = await buf.getFileChanges() expect(changes).toEqual([{ lnum: 2, filepath: '/a.ts', lines: [' })', ' } '] }]) }) it('should get changes #3', async () => { let lines = ` \u3000/a.ts }) } \u3000` let buf = await refactor.fromLines(lines.split('\n')) let changes = await buf.getFileChanges() expect(changes).toEqual([{ lnum: 2, filepath: '/a.ts', lines: [' })', ' }'] }]) }) it('should get changes #4', async () => { let lines = ` \u3000/a.ts foo \u3000/b.ts bar \u3000` let buf = await refactor.fromLines(lines.split('\n')) let changes = await buf.getFileChanges() expect(changes).toEqual([ { filepath: '/a.ts', lnum: 2, lines: ['foo'] }, { filepath: '/b.ts', lnum: 4, lines: ['bar'] } ]) }) }) describe('createRefactorBuffer()', () => { it('should create refactor buffer', async () => { let winid = await nvim.call('win_getid') as number let buf = await refactor.createRefactorBuffer() let curr = await nvim.call('win_getid') expect(curr).toBeGreaterThan(winid) let valid = await buf.valid expect(valid).toBe(true) buf = await refactor.createRefactorBuffer('vim') valid = await buf.valid expect(valid).toBe(true) }) it('should use conceal for line numbers', async () => { let buf = await refactor.createRefactorBuffer(undefined, true) let fileItem: FileItemDef = { filepath: __filename, ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }] } await buf.addFileItems([fileItem]) let arr = await nvim.call('getmatches') as any[] arr = arr.filter(o => o.group == 'Conceal') expect(arr.length).toBeGreaterThan(0) await buf.addFileItems([{ filepath: __filename, ranges: [{ start: 1, end: 3 }] }]) await nvim.command('normal! ggdG') let doc = workspace.getDocument(buf.bufnr) await doc.synchronize() let b = nvim.createBuffer(buf.bufnr) let res = await b.getVar('line_infos') expect(res).toEqual({}) }) }) describe('splitOpen()', () => { async function setup(): Promise { let buf = await refactor.createRefactorBuffer() let fileItem: FileItemDef = { filepath: __filename, ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }] } await buf.addFileItems([fileItem]) await nvim.call('cursor', [5, 1]) return buf } it('should jump to position by ', async () => { let buf = await setup() await buf.splitOpen() let line = await nvim.eval('line(".")') let bufname = await nvim.eval('bufname("%")') expect(bufname).toMatch('refactor.test.ts') expect(line).toBe(11) }) it('should jump split window when original window not valid', async () => { let win = await nvim.window let buf = await setup() await nvim.call('nvim_win_close', [win.id, true]) await buf.splitOpen() let line = await nvim.eval('line(".")') let bufname = await nvim.eval('bufname("%")') expect(bufname).toMatch('refactor.test.ts') expect(line).toBe(11) }) }) describe('showMenu()', () => { async function setup(): Promise { let buf = await refactor.createRefactorBuffer() let fileItem: FileItemDef = { filepath: __filename, ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }] } await buf.addFileItems([fileItem]) await nvim.call('cursor', [5, 1]) return buf } it('should do nothing when cancelled or range not found', async () => { let buf = await setup() let p = buf.showMenu() await helper.waitPrompt() await nvim.input('') await p let bufnr = await nvim.call('bufnr', ['%']) expect(bufnr).toBe(buf.bufnr) await nvim.call('cursor', [1, 1]) p = buf.showMenu() await helper.waitPrompt() await nvim.input('1') await p bufnr = await nvim.call('bufnr', ['%']) expect(bufnr).toBe(buf.bufnr) }) it('should open file in new tab', async () => { let buf = await setup() await nvim.call('cursor', [4, 1]) let p = buf.showMenu() await helper.waitPrompt() await nvim.input('1') await p let nr = await nvim.call('tabpagenr') expect(nr).toBe(2) let lnum = await nvim.call('line', ['.']) expect(lnum).toBe(11) }) it('should remove current block', async () => { let buf = await setup() await nvim.call('cursor', [4, 1]) let p = buf.showMenu() await helper.waitPrompt() await nvim.input('2') await p let items = buf.fileItems expect(items[0].ranges.length).toBe(1) await assertSynchronized(buf) }) }) describe('saveRefactor()', () => { it('should adjust line ranges after change', async () => { let filename = await createTmpFile('foo\n\nbar\n') let fileItem: FileItemDef = { filepath: filename, ranges: [{ start: 0, end: 1 }, { start: 2, end: 3 }] } let buf = await refactor.createRefactorBuffer() const getRanges = () => { let items = buf.fileItems let item = items.find(o => o.filepath == filename) return item.ranges.map(o => { return [o.start, o.start + o.lines.length] }) } await buf.addFileItems([fileItem, { filepath: __filename, ranges: [{ start: 1, end: 5 }] }]) expect(getRanges()).toEqual([[0, 1], [2, 3]]) nvim.pauseNotification() nvim.call('setline', [5, ['xyoo']], true) nvim.command('undojoin', true) nvim.call('append', [5, ['de']], true) nvim.command('undojoin', true) nvim.call('setline', [9, ['b']], true) await nvim.resumeNotification() let doc = workspace.getDocument(buf.bufnr) await doc.synchronize() let res = await helper.doAction('saveRefactor', doc.bufnr) expect(res).toBe(true) expect(getRanges()).toEqual([[0, 2], [3, 4]]) let content = fs.readFileSync(filename, 'utf8') expect(content).toBe('xyoo\nde\n\nb\n') }) it('should not save when no change made', async () => { let buf = await refactor.createRefactorBuffer() let fileItem: FileItemDef = { filepath: __filename, ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }] } await buf.addFileItems([fileItem]) let res = await buf.save() expect(res).toBe(false) }) it('should sync buffer change to file', async () => { let doc = await helper.createDocument() await doc.buffer.replace(['foo', 'bar', 'line'], 0) await helper.wait(30) let filename = URI.parse(doc.uri).fsPath let fileItem: FileItemDef = { filepath: filename, ranges: [{ start: 0, end: 2 }] } let buf = await refactor.createRefactorBuffer() await buf.addFileItems([fileItem]) await nvim.call('setline', [5, 'changed']) let res = await buf.save() expect(res).toBe(true) expect(fs.existsSync(filename)).toBe(true) let content = fs.readFileSync(filename, 'utf8') let lines = content.split('\n') expect(lines).toEqual(['changed', 'bar', 'line', '']) fs.unlinkSync(filename) }) }) describe('doRefactor', () => { let disposable: Disposable afterEach(() => { if (disposable) disposable.dispose() disposable = null }) it('should throw when rename provider not found', async () => { await helper.createDocument() let err try { await refactor.doRefactor() } catch (e) { err = e } expect(err).toBeDefined() }) it('should show message when prepare failed', async () => { await helper.createDocument() disposable = languages.registerRenameProvider(['*'], { prepareRename: () => { return undefined }, provideRenameEdits: () => { return null } }) await helper.doAction('refactor') let res = await helper.getCmdline() expect(res).toMatch(/Error/) }) it('should show message when returned edits is null', async () => { await helper.createDocument() disposable = languages.registerRenameProvider(['*'], { provideRenameEdits: () => { return null } }) await refactor.doRefactor() let res = await helper.getCmdline() expect(res).toMatch(/returns null/) }) it('should open refactor window when edits is valid', async () => { let filepath = __filename disposable = languages.registerRenameProvider(['*'], { provideRenameEdits: () => { let changes = { [URI.file(filepath).toString()]: [{ range: Range.create(0, 0, 0, 6), newText: '' }, { range: Range.create(1, 0, 1, 6), newText: '' }] } let edit: WorkspaceEdit = { changes } return edit } }) await helper.createDocument(filepath) let winid = await nvim.call('win_getid') as number await refactor.doRefactor() let currWin = await nvim.call('win_getid') as number expect(currWin - winid).toBeGreaterThan(0) let bufnr = await nvim.call('bufnr', ['%']) as number let b = refactor.getBuffer(bufnr) expect(b).toBeDefined() }) }) describe('search', () => { it('should open refactor buffer from search result', async () => { let escaped = await nvim.call('fnameescape', [__dirname]) await nvim.command(`cd ${escaped}`) await helper.createDocument() await refactor.search(['registerRenameProvider']) let buf = await nvim.buffer let name = await buf.name expect(name).toMatch(/__coc_refactor__/) let lines = await buf.lines expect(lines[0]).toMatch(/Save current buffer/) }) }) }) ================================================ FILE: src/__tests__/handler/rename.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import commands from '../../commands' import Rename from '../../handler/rename' import languages from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let rename: Rename beforeAll(async () => { await helper.setup() nvim = helper.nvim rename = helper.plugin.getHandler().rename }) function getWordRangeAtPosition(doc: TextDocument, position: Position): Range | null { let lines = doc.getText().split(/\r?\n/) let line = lines[position.line] if (line.length == 0 || position.character >= line.length) return null if (!/\w/.test(line[position.character])) return null let start = position.character let end = position.character + 1 if (!/\w/.test(line[start])) { return Range.create(position, { line: position.line, character: position.character + 1 }) } while (start >= 0) { let ch = line[start - 1] if (!ch || !/\w/.test(ch)) break start = start - 1 } while (end <= line.length) { let ch = line[end] if (!ch || !/\w/.test(ch)) break end = end + 1 } return Range.create(position.line, start, position.line, end) } function getSymbolRanges(textDocument: TextDocument, word: string): Range[] { let res: Range[] = [] let str = '' let content = textDocument.getText() for (let i = 0, l = content.length; i < l; i++) { let ch = content[i] if ('-' == ch && str.length == 0) { continue } let isKeyword = /\w/.test(ch) if (isKeyword) { str = str + ch } if (str.length > 0 && !isKeyword && str == word) { res.push(Range.create(textDocument.positionAt(i - str.length), textDocument.positionAt(i))) } if (!isKeyword) { str = '' } } return res } beforeEach(() => { disposables.push(languages.registerRenameProvider([{ language: 'javascript' }], { provideRenameEdits: (doc, position: Position, newName: string) => { let range = getWordRangeAtPosition(doc, position) if (range) { let word = doc.getText(range) if (word) { let ranges = getSymbolRanges(doc, word) return { changes: { [doc.uri]: ranges.map(o => TextEdit.replace(o, newName)) } } } } return undefined }, prepareRename: (doc, position) => { let range = getWordRangeAtPosition(doc, position) return range ? { range, placeholder: doc.getText(range) } : null } })) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) disposables = [] }) describe('rename handler', () => { describe('getWordEdit', () => { it('should not throw when provider not found', async () => { await helper.edit() let res = await helper.doAction('getWordEdit') expect(res).toBe(null) }) it('should use document symbols when prepare failed', async () => { let doc = await helper.createDocument('t.js') await nvim.setLine('a') await doc.synchronize() let res = await rename.getWordEdit() expect(res != null).toBe(true) }) it('should return workspace edit', async () => { let doc = await helper.createDocument('t.js') await nvim.setLine('foo foo') await doc.synchronize() let res = await rename.getWordEdit() expect(res).toBeDefined() expect(res.changes[doc.uri].length).toBe(2) }) it('should extract words from buffer', async () => { let doc = await helper.createDocument('t') await nvim.setLine('你 你 你') await doc.synchronize() let res = await rename.getWordEdit() expect(res).toBeDefined() expect(res.changes[doc.uri].length).toBe(3) }) }) describe('rename', () => { it('should throw when provider not found', async () => { await helper.edit() await expect(async () => { await helper.doAction('rename', 'foo') }).rejects.toThrow(Error) }) it('should return false for invalid position', async () => { let doc = await helper.createDocument('t.js') let res = await commands.executeCommand('editor.action.rename', [doc.uri, Position.create(0, 0)]) expect(res).toBe(false) }) it('should use newName from placeholder', async () => { let doc = await helper.createDocument('t.js') await nvim.setLine('foo foo foo') let p = commands.executeCommand('editor.action.rename', doc.uri, Position.create(0, 0)) await helper.waitFloat() await nvim.input('') await helper.wait(10) await nvim.input('bar') await nvim.input('') await p let line = await nvim.line expect(line).toBe('bar bar bar') }) it('should renameCurrentWord by cursors', async () => { await commands.executeCommand('document.renameCurrentWord') let line = await helper.getCmdline() expect(line).toMatch('Invalid position') let doc = await helper.createDocument('t.js') await nvim.setLine('foo foo foo') await commands.executeCommand('document.renameCurrentWord') let ns = await nvim.createNamespace('coc-cursors') let markers = await doc.buffer.getExtMarks(ns, 0, -1) expect(markers.length).toBe(3) }) it('should return false for empty name', async () => { helper.updateConfiguration('coc.preferences.renameFillCurrent', false) await helper.createDocument('t.js') await nvim.setLine('foo foo foo') let p = rename.rename() await helper.waitFloat() await nvim.input('') await helper.wait(10) await nvim.input('') let res = await p expect(res).toBe(false) }) it('should not throw when provideRenameEdits throws', async () => { disposables.push(languages.registerRenameProvider([{ language: '*' }], { provideRenameEdits: () => { throw new Error('error') }, })) let doc = await workspace.document let res = await languages.provideRenameEdits(doc.textDocument, Position.create(0, 0), 'newName', CancellationToken.None) expect(res).toBeNull() }) it('should use newName from range', async () => { disposables.push(languages.registerRenameProvider([{ language: '*' }], { provideRenameEdits: (doc, position: Position, newName: string) => { let range = getWordRangeAtPosition(doc, position) if (range) { let word = doc.getText(range) if (word) { let ranges = getSymbolRanges(doc, word) return { changes: { [doc.uri]: ranges.map(o => TextEdit.replace(o, newName)) } } } } return undefined }, prepareRename: (doc, position) => { let range = getWordRangeAtPosition(doc, position) return range ? range : null } })) await helper.createDocument() await nvim.setLine('foo foo foo') let p = rename.rename() await helper.waitFloat() await nvim.input('') await helper.wait(10) await nvim.input('bar') await nvim.input('') let res = await p expect(res).toBe(true) await helper.waitFor('getline', ['.'], 'bar bar bar') }) it('should use newName from cword', async () => { disposables.push(languages.registerRenameProvider([{ language: '*' }], { provideRenameEdits: (doc, position: Position, newName: string) => { let range = getWordRangeAtPosition(doc, position) if (range) { let word = doc.getText(range) if (word) { let ranges = getSymbolRanges(doc, word) return { changes: { [doc.uri]: ranges.map(o => TextEdit.replace(o, newName)) } } } } return undefined } })) await helper.createDocument() await nvim.setLine('foo foo foo') let p = rename.rename() await helper.waitFloat() await nvim.input('') await helper.wait(10) await nvim.input('bar') await nvim.input('') let res = await p expect(res).toBe(true) let line = await nvim.getLine() expect(line).toBe('bar bar bar') }) it('should return false when result is empty', async () => { disposables.push(languages.registerRenameProvider([{ language: '*' }], { provideRenameEdits: () => { return null } })) await helper.createDocument() await nvim.setLine('foo foo foo') let p = rename.rename() await helper.waitFloat() await nvim.input('') await helper.wait(10) await nvim.input('bar') await nvim.input('') let res = await p expect(res).toBe(false) }) }) }) ================================================ FILE: src/__tests__/handler/search.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import Refactor from '../../handler/refactor' import Search, { getPathFromArgs } from '../../handler/refactor/search' import helper from '../helper' import path from 'path' let nvim: Neovim let refactor: Refactor // use fake rg command let cmd = path.resolve(__dirname, '../rg') let cwd = process.cwd() beforeAll(async () => { await helper.setup() nvim = helper.nvim refactor = helper.plugin.getHandler().refactor }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { refactor.reset() await helper.reset() }) describe('getPathFromArgs', () => { it('should get undefined path', async () => { let res = getPathFromArgs(['a']) expect(res).toBeUndefined() res = getPathFromArgs(['a', 'b', '-c']) expect(res).toBeUndefined() res = getPathFromArgs(['a', '-b', 'c']) expect(res).toBeUndefined() }) }) describe('search', () => { it('should open refactor window', async () => { let search = new Search(nvim, cmd) let buf = await refactor.createRefactorBuffer() await search.run([], cwd, buf) await helper.wait(50) let fileItems = buf.fileItems expect(fileItems.length).toBe(2) expect(fileItems[0].ranges.length).toBe(2) }) it('should abort task', async () => { let search = new Search(nvim, cmd) let buf = await refactor.createRefactorBuffer() let p = search.run(['--sleep', '1000'], cwd, buf) search.abort() await p let fileItems = buf.fileItems expect(fileItems.length).toBe(0) }) it('should work with CocAction search', async () => { await helper.doAction('search', ['CocAction']) let bufnr = await nvim.call('bufnr', ['%']) as number let buf = refactor.getBuffer(bufnr) expect(buf).toBeDefined() }) it('should fail on invalid command', async () => { let search = new Search(nvim, 'rrg') let buf = await refactor.createRefactorBuffer() let err try { await search.run([], cwd, buf) } catch (e) { err = e } expect(err).toBeDefined() let msg = await helper.getCmdline() expect(msg).toMatch(/Error on command "rrg"/) }) it('should show empty result when no result found', async () => { await helper.doAction('search', ['should found ' + ' no result']) let bufnr = await nvim.call('bufnr', ['%']) as number let buf = refactor.getBuffer(bufnr) expect(buf).toBeDefined() let buffer = await nvim.buffer let lines = await buffer.lines expect(lines[1]).toMatch(/No match found/) }) it('should use current search folder for rg', async () => { let search = new Search(nvim, 'rg') await helper.createDocument() let buf = await refactor.createRefactorBuffer() await search.run(['-w', 'createRefactorBuffer', 'src/__tests__'], cwd, buf) let buffer = await nvim.buffer let lines = await buffer.lines expect(lines[1].startsWith('Files: ')).toBe(true) }) }) ================================================ FILE: src/__tests__/handler/selectionRange.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import SelectionRange from '../../handler/selectionRange' import languages from '../../languages' import workspace from '../../workspace' import window from '../../window' import { disposeAll } from '../../util' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let selection: SelectionRange beforeAll(async () => { await helper.setup() nvim = helper.nvim selection = helper.plugin.getHandler().selectionRange }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) }) describe('selectionRange', () => { describe('getSelectionRanges()', () => { it('should throw error when selectionRange provider does not exist', async () => { let doc = await helper.createDocument() await doc.synchronize() await expect(async () => { await helper.doAction('selectionRanges') }).rejects.toThrow(Error) }) it('should return ranges', async () => { await helper.createDocument() disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(0, 0, 0, 1) }] } })) let res = await selection.getSelectionRanges() expect(res).toBeDefined() expect(Array.isArray(res)).toBe(true) }) }) describe('selectRange()', () => { async function getSelectedRange(): Promise { let m = await nvim.mode expect(m.mode).toBe('v') await nvim.input('') let res = await window.getSelectedRange('v') return res } it('should not select with empty ranges', async () => { let doc = await helper.createDocument() disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: () => [] })) await doc.synchronize() let res = await selection.selectRange('', true) expect(res).toBe(false) }) it('should select single range', async () => { let doc = await helper.createDocument() await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\ntest\n')]) disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: () => [{ range: Range.create(0, 0, 0, 3) }] })) await doc.synchronize() let res = await selection.selectRange('', true) expect(res).toBe(true) }) it('should select ranges forward', async () => { let doc = await helper.createDocument() let called = 0 await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\ntest\n')]) await nvim.call('cursor', [1, 1]) await doc.synchronize() disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { called += 1 let arr = [{ range: Range.create(0, 0, 0, 1) }, { range: Range.create(0, 0, 0, 3) }, { range: Range.create(0, 0, 1, 3) }] return arr } })) await doc.synchronize() await helper.doAction('rangeSelect', '', false) await selection.selectRange('', true) expect(called).toBe(1) let res = await getSelectedRange() expect(res).toEqual(Range.create(0, 0, 0, 1)) await selection.selectRange('v', true) expect(called).toBe(2) res = await getSelectedRange() expect(res).toEqual(Range.create(0, 0, 0, 3)) await selection.selectRange('v', true) expect(called).toBe(3) res = await getSelectedRange() expect(res).toEqual(Range.create(0, 0, 1, 3)) await selection.selectRange('v', true) expect(called).toBe(4) let m = await nvim.mode expect(m.mode).toBe('n') }) it('should select ranges backward', async () => { let doc = await helper.createDocument() await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\ntest\n')]) await nvim.call('cursor', [1, 1]) disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { let arr = [{ range: Range.create(0, 0, 0, 1) }, { range: Range.create(0, 0, 0, 3) }, { range: Range.create(0, 0, 1, 3) }] return arr } })) await doc.synchronize() await selection.selectRange('', true) let mode = await nvim.call('mode') expect(mode).toBe('v') await nvim.input('') await window.selectRange(Range.create(0, 0, 1, 3)) await nvim.input('') await selection.selectRange('v', false) let r = await getSelectedRange() expect(r).toEqual(Range.create(0, 0, 0, 3)) await nvim.input('') await selection.selectRange('v', false) r = await getSelectedRange() expect(r).toEqual(Range.create(0, 0, 0, 1)) await nvim.input('') await selection.selectRange('v', false) mode = await nvim.call('mode') expect(mode).toBe('n') }) }) describe('provideSelectionRanges()', () => { it('should return null when no provider available', async () => { let doc = await workspace.document let res = await languages.getSelectionRanges(doc.textDocument, [Position.create(0, 0)], CancellationToken.None) expect(res).toBeNull() }) it('should return null when no result available', async () => { disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [] } })) let doc = await workspace.document let res = await languages.getSelectionRanges(doc.textDocument, [Position.create(0, 0)], CancellationToken.None) expect(res).toBeNull() }) it('should append/prepend selection ranges', async () => { let doc = await workspace.document disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(1, 1, 1, 4) }, { range: Range.create(1, 0, 1, 6) }] } })) disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(1, 2, 1, 3) }] } })) disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(1, 2, 1, 3) }] } })) disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { provideSelectionRanges: _doc => { return [{ range: Range.create(0, 0, 3, 0) }] } })) let res = await languages.getSelectionRanges(doc.textDocument, [Position.create(0, 0)], CancellationToken.None) expect(res.length).toBe(4) expect(res[0].range).toEqual(Range.create(1, 2, 1, 3)) expect(res[3].range).toEqual(Range.create(0, 0, 3, 0)) }) }) }) ================================================ FILE: src/__tests__/handler/semanticTokens.test.ts ================================================ import { Buffer, Neovim } from '@chemzqm/neovim' import fs from 'fs' import { tmpdir } from 'os' import path from 'path' import { CancellationToken, CancellationTokenSource, Disposable, Position, Range, SemanticTokensLegend, TextEdit } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import commandManager from '../../commands' import events from '../../events' import SemanticTokensBuffer, { NAMESPACE, toHighlightPart } from '../../handler/semanticTokens/buffer' import SemanticTokens from '../../handler/semanticTokens/index' import languages from '../../languages' import { disposeAll } from '../../util' import { CancellationError } from '../../util/errors' import window from '../../window' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'coc')) let nvim: Neovim let ns: number let disposables: Disposable[] = [] let semanticTokens: SemanticTokens let legend: SemanticTokensLegend = { tokenTypes: [ "comment", "keyword", "string", "number", "regexp", "operator", "namespace", "type", "struct", "class", "interface", "enum", "enumMember", "typeParameter", "function", "method", "property", "macro", "variable", "parameter", "angle", "arithmetic", "attribute", "bitwise", "boolean", "brace", "bracket", "builtinType", "character", "colon", "comma", "comparison", "constParameter", "dot", "escapeSequence", "formatSpecifier", "generic", "label", "lifetime", "logical", "operator", "parenthesis", "punctuation", "selfKeyword", "semicolon", "typeAlias", "union", "unresolvedReference" ], tokenModifiers: [ "documentation", "declaration", "definition", "static", "abstract", "deprecated", "readonly", "constant", "controlFlow", "injected", "mutable", "consuming", "async", "library", "public", "unsafe", "attribute", "trait", "callable", "intraDocLink" ] } beforeAll(async () => { await helper.setup() nvim = helper.nvim ns = await nvim.createNamespace('coc-semanticTokens') semanticTokens = helper.plugin.getHandler().semanticHighlighter }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) semanticTokens.setStaticConfiguration() }) const defaultResult = { resultId: '1', data: [ 0, 0, 2, 1, 0, 0, 3, 4, 14, 2, 0, 4, 1, 41, 0, 0, 1, 1, 41, 3, 0, 2, 1, 25, 0, 1, 4, 8, 17, 0, 0, 8, 1, 41, 0, 0, 1, 3, 2, 0, 0, 3, 1, 41, 0, 0, 1, 1, 44, 0, 1, 0, 1, 25, 0, ] } async function waitRefresh(tokenBuffer: SemanticTokensBuffer): Promise { return new Promise((resolve, reject) => { let timer = setTimeout(() => { disposable.dispose() reject(new Error(`Timeout after 500ms`)) }, 500) let disposable = tokenBuffer.onDidRefresh(() => { disposable.dispose() clearTimeout(timer) resolve() }) }) } function registerRangeProvider(filetype: string, fn: (range: Range) => number[]): Disposable { return languages.registerDocumentRangeSemanticTokensProvider([{ language: filetype }], { provideDocumentRangeSemanticTokens: (_, range) => { return { data: fn(range) } } }, legend) } function registerProvider(): void { disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'rust' }], { provideDocumentSemanticTokens: () => { return defaultResult }, provideDocumentSemanticTokensEdits: (_, previousResultId) => { if (previousResultId !== '1') return undefined return { resultId: '2', edits: [{ start: 0, deleteCount: 0, data: [0, 0, 3, 1, 0] }] } } }, legend)) } async function createRustBuffer(enableProvider = true): Promise { helper.updateConfiguration('semanticTokens.filetypes', ['rust']) if (enableProvider) registerProvider() await helper.wait(2) let doc = await workspace.document let code = `fn main() { println!("H"); }` let buf = await nvim.buffer doc.setFiletype('rust') await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() return buf } describe('semanticTokens', () => { describe('toHighlightPart()', () => { it('should convert to highlight part', () => { expect(toHighlightPart('')).toBe('') expect(toHighlightPart('token')).toBe('Token') expect(toHighlightPart('is key word')).toBe('Is_key_word') expect(toHighlightPart('token')).toBe('Token') }) }) describe('Provider', () => { it('should not throw when buffer item not found', async () => { await events.fire('CursorMoved', [9]) await events.fire('BufWinEnter', [9]) }) it('should return null when range provider not exists', async () => { let doc = await workspace.document let res = await languages.provideDocumentRangeSemanticTokens(doc.textDocument, Range.create(0, 0, 1, 0), CancellationToken.None) expect(res).toBeNull() }) it('should return false when not hasSemanticTokensEdits', async () => { let doc = await workspace.document let res = languages.hasSemanticTokensEdits(doc.textDocument) expect(res).toBe(false) }) it('should return null when semanticTokens provider not exists', async () => { let token = CancellationToken.None let doc = await workspace.document let res = await languages.provideDocumentSemanticTokens(doc.textDocument, token) expect(res).toBeNull() let r = await languages.provideDocumentSemanticTokensEdits(doc.textDocument, '', token) expect(r).toBeNull() }) }) describe('showHighlightInfo()', () => { it('should show error when not enabled', async () => { await nvim.command('enew') let doc = await workspace.document let winid = await nvim.call('win_getid') as number let item = semanticTokens.getItem(doc.bufnr) await item.onCursorHold(winid, 1) await semanticTokens.inspectSemanticToken() let line = await helper.getCmdline() expect(line).toMatch('not enabled') }) it('should show error message for buffer not attached', async () => { await nvim.command(`edit +setl\\ buftype=nofile foo`) await helper.doAction('inspectSemanticToken') let msg = await helper.getCmdline() expect(msg).toMatch(/not attached/) }) it('should show message when not enabled', async () => { await helper.edit('t.txt') await helper.doAction('showSemanticHighlightInfo') let buf = await nvim.buffer let lines = await buf.lines expect(lines[2]).toMatch('not enabled for current filetype') }) it('should show semantic tokens info', async () => { await createRustBuffer() await semanticTokens.highlightCurrent() await commandManager.executeCommand('semanticTokens.checkCurrent') let buf = await nvim.buffer let lines = await buf.lines let content = lines.join('\n') expect(content).toMatch('Semantic highlight groups used by current buffer') }) it('should show highlight info for empty legend', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['*']) disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: (_, range) => { return { data: [] } } }, { tokenModifiers: [], tokenTypes: [] })) await semanticTokens.showHighlightInfo() let buf = await nvim.buffer let lines = await buf.lines let content = lines.join('\n') expect(content).toMatch('No token') }) }) describe('highlightCurrent()', () => { it('should only highlight limited range on update', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['vim']) let doc = await helper.createDocument('t.vim') let called = false disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], { provideDocumentSemanticTokens: (doc, token) => { let text = doc.getText() if (!text.trim()) { return Promise.resolve({ resultId: '1', data: [] }) } let lines = text.split('\n') let data = [0, 0, 1, 1, 0] for (let i = 0; i < lines.length; i++) { data.push(1, 0, 1, 1, 0) } return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) resolve(undefined) }) let timer = setTimeout(() => { called = true resolve({ resultId: '1', data }) }, 10) }) } }, legend)) let item = await semanticTokens.getCurrentItem() item['_dirty'] = true await item.doHighlight(false, 0) let newLine = 'l\n' await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: `${newLine.repeat(1000)}` }]) await item.doHighlight(false, 0) await helper.waitValue(() => called, true) let buf = doc.buffer let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) let len = markers.length expect(len).toBeLessThan(400) await nvim.call('cursor', [1, 1]) let winid = await nvim.call('win_getid') as number await item.onWinScroll(winid) await helper.waitValue(async () => { let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) return markers.length > 100 }, true) await nvim.call('cursor', [200, 1]) await item.onWinScroll(winid) await helper.waitValue(async () => { let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) return markers.length > 200 }, true) }) it('should refresh highlights', async () => { await createRustBuffer() await nvim.command('hi link CocSemDeclarationFunction MoreMsg') await nvim.command('hi link CocSemDocumentation Statement') await window.moveTo({ line: 0, character: 4 }) await semanticTokens.highlightCurrent() await commandManager.executeCommand('semanticTokens.inspect') let win = await helper.getFloat() let buf = await win.buffer let lines = await buf.lines let content = lines.join('\n') expect(content).toMatch('Type: function\nModifiers: declaration\nHighlight group: CocSemTypeFunction') await window.moveTo({ line: 1, character: 0 }) await commandManager.executeCommand('semanticTokens.inspect') win = await helper.getFloat() expect(win).toBeUndefined() }) it('should refresh highlights by command', async () => { await helper.edit() let err try { await commandManager.executeCommand('semanticTokens.refreshCurrent') } catch (e) { err = e } expect(err).toBeDefined() }) it('should reuse exists tokens when version not changed', async () => { let doc = await helper.createDocument('t.vim') await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }]) let times = 0 helper.updateConfiguration('semanticTokens.filetypes', ['vim']) disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], { provideDocumentSemanticTokens: () => { times++ return new Promise(resolve => { resolve({ resultId: '1', data: [0, 0, 3, 1, 0] }) }) } }, legend)) let item = await semanticTokens.getCurrentItem() await helper.waitValue(() => { return times }, 1) await item.doHighlight(false, 0) await item.doHighlight(false, 0) expect(times).toBe(1) }) it('should return null when request cancelled', async () => { let doc = await helper.createDocument('t.vim') let lines: string[] = [] for (let i = 0; i < 2000; i++) { lines.push('foo') } await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: lines.join('\n') }]) helper.updateConfiguration('semanticTokens.filetypes', []) let cancel = true let item = await semanticTokens.getCurrentItem() disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], { provideDocumentSemanticTokens: (doc, token) => { return new Promise(resolve => { if (cancel) { process.nextTick(() => { item.cancel() }) } let data = [] for (let i = 0; i < 2000; i++) { data.push(...[i == 0 ? 0 : 1, 0, 3, 1, 1]) } resolve({ resultId: '1', data }) }) } }, legend)) helper.updateConfiguration('semanticTokens.filetypes', ['vim']) await item.doHighlight(false, 0) cancel = false let spy = jest.spyOn(window, 'diffHighlights').mockImplementation(() => { return Promise.resolve(null) }) let winid = await nvim.call('win_getid') as number await item.doHighlight(false, 10, winid) await item.doHighlight(false, 0, winid) spy.mockRestore() expect(item.highlights).toBeDefined() await helper.edit('bar') }) it('should highlight hidden buffer on shown', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['rust']) registerProvider() await nvim.command('edit foo') let code = 'fn main() {\n println!("H"); \n}' let filepath = path.join(tempDir, 'a.rs') fs.writeFileSync(filepath, code, 'utf8') let uri = URI.file(filepath).toString() await workspace.loadFile(uri, '') let doc = workspace.getDocument(uri) await nvim.command('b ' + doc.bufnr) let item = semanticTokens.getItem(doc.bufnr) let called = false item.onDidRefresh(() => { called = true }) let buf = doc.buffer expect(doc.filetype).toBe('rust') await nvim.command(`b ${buf.id}`) await helper.waitValue(() => { return called }, true) }) it('should no highlights when request cancelled', async () => { helper.updateConfiguration('semanticTokens.filetypes', []) let doc = await workspace.document let item = semanticTokens.getItem(doc.bufnr) disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: () => { item.cancel() return null } }, legend)) let disposable = languages.registerDocumentSemanticTokensProvider([{ language: '*' }], { provideDocumentSemanticTokens: (_, token) => { item.cancel() return null } }, legend) helper.updateConfiguration('semanticTokens.filetypes', ['*']) await item.doHighlight(true, 0) expect(item.highlights).toBeUndefined() disposable.dispose() let winid = await nvim.call('win_getid') as number await item.doHighlight(true) await item.onWinScroll(winid) }) }) describe('highlightRegions()', () => { it('should refresh when buffer visible', async () => { let buf = await createRustBuffer(false) let doc = await workspace.document let item = await semanticTokens.getCurrentItem() let winid = await nvim.call('win_getid') as number await item.highlightRegions(winid, CancellationToken.None) await doc.synchronize() expect(item.enabled).toBe(false) await nvim.command('edit bar') registerProvider() await helper.wait(10) expect(item.enabled).toBe(true) await nvim.command(`b ${buf.id}`) await waitRefresh(item) expect(item.highlights).toBeDefined() await item.highlightRegions(9999, CancellationToken.None) }) it('should not highlight same region', async () => { let buf = await createRustBuffer() let item = semanticTokens.getItem(buf.id) let winid = await nvim.call('win_getid') as number await item.doHighlight(false, 0) await item.highlightRegions(winid, CancellationToken.None) await item.highlightRegions(winid, CancellationToken.None) }) it('should highlight region on CursorHold', async () => { let buf = await createRustBuffer() let item = semanticTokens.getItem(buf.id) let winid = await nvim.call('win_getid') as number await item.doHighlight(true, 0, winid) buf.clearNamespace(NAMESPACE) await item.onCursorHold(winid, 1) let highlights = await buf.getHighlights(NAMESPACE) expect(highlights.length).toBeGreaterThan(0) }) it('should cancel region highlight', async () => { let buf = await createRustBuffer() let item = semanticTokens.getItem(buf.id) await item.doHighlight(false, 0) let tokenSource = new CancellationTokenSource() let spy = jest.spyOn(window, 'diffHighlights').mockImplementation(() => { tokenSource.cancel() return Promise.resolve(null) }) let winid = await nvim.call('win_getid') as number await item.highlightRegions(winid, tokenSource.token) spy.mockRestore() }) }) describe('requestRangeHighlights()', () => { it('should return null when canceled', async () => { let doc = await workspace.document let item = semanticTokens.getItem(doc.bufnr) let winid = await nvim.call('win_getid') as number let res = await item.requestRangeHighlights(winid, undefined, CancellationToken.Cancelled) expect(res).toBeNull() let tokenSource = new CancellationTokenSource() disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: () => { tokenSource.cancel() return { data: [] } } }, legend)) res = await item.requestRangeHighlights(winid, undefined, tokenSource.token) expect(res).toBeNull() }) it('should return null when convert tokens canceled ', async () => { let doc = await workspace.document let item = semanticTokens.getItem(doc.bufnr) let tokenSource = new CancellationTokenSource() disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: () => { return { data: [1, 0, 0, 1, 0] } } }, legend)) let spy = jest.spyOn(item, 'getTokenRanges').mockImplementation(() => { return Promise.resolve(null) }) let winid = await nvim.call('win_getid') as number let res = await item.requestRangeHighlights(winid, undefined, tokenSource.token) expect(res).toBeNull() spy.mockRestore() }) }) describe('clear highlights', () => { it('should clear highlights of current buffer', async () => { await createRustBuffer() await semanticTokens.highlightCurrent() let buf = await nvim.buffer let markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBeGreaterThan(0) await commandManager.executeCommand('semanticTokens.clearCurrent') markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBe(0) }) it('should clear all highlights', async () => { await createRustBuffer() await semanticTokens.highlightCurrent() let buf = await nvim.buffer await commandManager.executeCommand('semanticTokens.clearAll') let markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBe(0) }) }) describe('doRangeHighlight()', () => { it('should invoke range provider first time when both kinds exist', async () => { let called = false disposables.push(registerRangeProvider('rust', () => { called = true return [] })) let buf = await createRustBuffer() let item = semanticTokens.getItem(buf.id) await waitRefresh(item) expect(called).toBe(true) }) it('should do range highlight first time', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['vim']) let r: Range disposables.push(registerRangeProvider('vim', range => { r = range return [0, 0, 3, 1, 0] })) let filepath = await createTmpFile('let') fs.renameSync(filepath, filepath + '.vim') let doc = await helper.createDocument(filepath + '.vim') let item = await semanticTokens.getCurrentItem() await doc.synchronize() expect(doc.filetype).toBe('vim') await helper.waitValue(() => { return typeof r !== 'undefined' }, true) let winid = await nvim.call('win_getid') as number await item.onWinScroll(winid) }) it('should do range highlight after cursor moved', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['vim']) let doc = await helper.createDocument(`95cb98ca-df0a-4cac-9cd3-2459db259b71.vim`) await nvim.call('cursor', [1, 1]) let r: Range expect(doc.filetype).toBe('vim') await nvim.call('setline', [2, (new Array(200).fill(''))]) await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }]) disposables.push(registerRangeProvider('vim', range => { r = range return [] })) let item = semanticTokens.getItem(doc.bufnr) item.cancel() nvim.call('cursor', [201, 1], true) await helper.waitValue(() => { return r && r.end.line > 200 }, true) }) it('should not throw when range request throws', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['*']) let doc = await workspace.document let called = false disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: (_, range) => { called = true throw new Error('custom error') } }, legend)) await helper.wait(2) let item = semanticTokens.getItem(doc.bufnr) let winid = await nvim.call('win_getid') as number await item.doRangeHighlight(winid, undefined, CancellationToken.None) expect(called).toBe(true) }) it('should only cancel range highlight request', async () => { let rangeCancelled = false disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: 'vim' }], { provideDocumentRangeSemanticTokens: (_, range, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timeout) rangeCancelled = true resolve(null) }) let timeout = setTimeout(() => { resolve({ data: [] }) }, 500) }) } }, legend)) disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], { provideDocumentSemanticTokens: (_, token) => { return new Promise(resolve => { resolve({ resultId: '1', data: [0, 0, 3, 1, 0] }) }) } }, legend)) let doc = await helper.createDocument('t.vim') await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }]) let item = await semanticTokens.getCurrentItem() helper.updateConfiguration('semanticTokens.filetypes', ['vim']) item.cancel() let p = item.doHighlight(false, 0) await helper.wait(10) item.cancel(true) await p expect(rangeCancelled).toBe(true) }) it('should do range highlight on CursorHold', async () => { helper.updateConfiguration('semanticTokens.filetypes', ['vim']) disposables.push(registerRangeProvider('vim', range => { return [0, 0, 3, 1, 0] })) await helper.wait(10) let doc = await helper.createDocument('t.vim') await nvim.call('cursor', [1, 1]) await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }]) let item = semanticTokens.getItem(doc.bufnr) item.cancel() let winid = await nvim.call('win_getid') as number doc.buffer.clearNamespace(NAMESPACE) await item.onCursorHold(winid, 1) let highlights = await doc.buffer.getHighlights(NAMESPACE) expect(highlights.length).toBe(1) }) }) describe('triggerSemanticTokens', () => { it('should be disabled by default', async () => { helper.updateConfiguration('semanticTokens.filetypes', []) await workspace.document const curr = await semanticTokens.getCurrentItem() expect(curr.enabled).toBe(false) }) it('should be enabled', async () => { await createRustBuffer() const curr = await semanticTokens.getCurrentItem() expect(curr.enabled).toBe(true) }) it('should get legend by API', async () => { await createRustBuffer() const doc = await workspace.document const l = languages.getLegend(doc.textDocument) expect(l).toEqual(legend) }) it('should doHighlight', async () => { await createRustBuffer() const doc = await workspace.document await nvim.call('CocAction', 'semanticHighlight') const highlights = await doc.buffer.getHighlights(NAMESPACE) expect(highlights.length).toBeGreaterThan(0) expect(highlights[0].hlGroup).toBe('CocSemTypeKeyword') }) }) describe('delta update', () => { it('should perform highlight update', async () => { await createRustBuffer() let buf = await nvim.buffer await semanticTokens.highlightCurrent() await window.moveTo({ line: 0, character: 0 }) let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) let curr = await semanticTokens.getCurrentItem() await curr.requestAllHighlights(CancellationToken.None, false) let markers = await buf.getExtMarks(ns, 0, -1, {}) expect(markers.length).toBeGreaterThan(0) }) }) describe('checkState', () => { it('should throw for invalid state', async () => { let doc = await workspace.document const toThrow = (cb: () => void) => { expect(cb).toThrow(Error) } let item = semanticTokens.getItem(doc.bufnr) toThrow(() => { item.checkState() }) helper.updateConfiguration('semanticTokens.filetypes', ['*']) toThrow(() => { item.checkState() }) toThrow(() => { item.checkState() }) let enabled = item.enabled expect(enabled).toBe(false) expect(() => { item.checkState() }).toThrow('provider not found') registerProvider() }) }) describe('enabled', () => { it('should check if buffer enabled for semanticTokens', async () => { let doc = await workspace.document let item = semanticTokens.getItem(doc.bufnr) disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: (_, range) => { return { data: [] } } }, { tokenModifiers: [], tokenTypes: [] })) await helper.wait(2) let winid = await nvim.call('win_getid') as number await item.onShown(winid) expect(item.enabled).toBe(false) helper.updateConfiguration('semanticTokens.filetypes', ['vim']) expect(item.enabled).toBe(false) helper.updateConfiguration('semanticTokens.filetypes', ['*']) expect(item.enabled).toBe(true) }) it('should toggle enable by configuration', async () => { helper.updateConfiguration('semanticTokens.enable', false) let buf = await createRustBuffer() let item = semanticTokens.getItem(buf.id) helper.updateConfiguration('semanticTokens.enable', true) await waitRefresh(item) let markers = await buf.getExtMarks(ns, 0, -1, {}) expect(markers.length).toBeGreaterThan(0) helper.updateConfiguration('semanticTokens.enable', false) markers = await buf.getExtMarks(ns, 0, -1, {}) expect(markers.length).toBe(0) helper.updateConfiguration('semanticTokens.enable', true) }) }) describe('Server cancelled', () => { beforeEach(() => { helper.updateConfiguration('semanticTokens.filetypes', ['*']) }) it('should retrigger range request on server cancel', async () => { let times = 0 disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], { provideDocumentRangeSemanticTokens: () => { times++ if (times == 1) { throw new CancellationError() } return { data: [] } } }, { tokenModifiers: [], tokenTypes: [] })) await helper.waitValue(() => { return times > 1 }, true) }) it('should retrigger full request on server cancel', async () => { helper.updateConfiguration('semanticTokens.enable', true) await workspace.document let times = 0 disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: '*' }], { provideDocumentSemanticTokens: () => { times++ if (times == 1) { throw new CancellationError() } return { data: [] } } }, { tokenModifiers: [], tokenTypes: [] })) await helper.waitValue(() => { return times }, 2) }) }) }) ================================================ FILE: src/__tests__/handler/signature.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable, ParameterInformation, Range, SignatureInformation } from 'vscode-languageserver-protocol' import commands from '../../commands' import events from '../../events' import Signature from '../../handler/signature' import languages from '../../languages' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let signature: Signature let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim signature = helper.plugin.getHandler().signature }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) disposables = [] }) describe('signatureHelp', () => { describe('triggerSignatureHelp', () => { it('should show signature by api', async () => { let res = await signature.triggerSignatureHelp() expect(res).toBe(false) disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } })) await helper.createDocument() await nvim.input('foo') await commands.executeCommand('editor.action.triggerParameterHints') let win = await helper.getFloat() expect(win).toBeDefined() let lines = await helper.getWinLines(win.id) expect(lines[2]).toMatch('my signature') }) it('should load configuration', async () => { await nvim.command(`edit +setl\\ buftype=nofile tree`) signature.loadConfiguration() }) it('should use 0 when activeParameter is undefined', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo(a)', 'my signature', { label: 'a' })], activeParameter: undefined, activeSignature: null } } }, [])) await helper.createDocument() await nvim.input('foo') await helper.doAction('showSignatureHelp') await signature.triggerSignatureHelp() let win = await helper.getFloat() expect(win).toBeDefined() let buf = await win.buffer let hls = await buf.getHighlights(-1 as any) expect(hls.length).toBe(2) expect(hls[0].hlGroup).toBe('CocFloatActive') }) it('should trigger by space', async () => { let promise = new Promise(resolve => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { resolve(undefined) return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } }, [' '])) }) await helper.createDocument() await nvim.input('i') await helper.wait(30) await nvim.input(' ') await promise }) it('should show signature help with param label as string', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [ SignatureInformation.create('foo()', 'my signature'), SignatureInformation.create('foo', 'my signature', ParameterInformation.create('a', 'description')), ], activeParameter: 0, activeSignature: 1 } } }, [])) await helper.createDocument() await nvim.input('foo') await signature.triggerSignatureHelp() let win = await helper.getFloat() expect(win).toBeDefined() let lines = await helper.getWinLines(win.id) expect(lines.join('\n')).toMatch(/description/) }) }) describe('events', () => { function registProvider(): void { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo(x, y)', 'my signature')], activeParameter: 0, activeSignature: 0 } } }, ['(', ','])) } it('should trigger signature help on TextInsert', async () => { registProvider() await helper.createDocument() await nvim.input('ifoo') await nvim.input('(') await helper.waitValue(async () => { let win = await helper.getFloat() return win != null }, true) let win = await helper.getFloat() let lines = await helper.getWinLines(win.id) expect(lines[2]).toMatch('my signature') }) it('should trigger signature help on PlaceholderJump', async () => { let called = 0 disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { called += 1 return { signatures: [SignatureInformation.create('foo(x, y)', 'my signature')], activeParameter: 0, activeSignature: 0 } } }, ['(', ','])) let doc = await helper.createDocument() Object.assign((workspace as any)._env, { jumpAutocmd: true }) await events.fire('PlaceholderJump', [doc.bufnr, { charbefore: ' ', range: Range.create(0, 0, 0, 0) }]) Object.assign((workspace as any)._env, { jumpAutocmd: false }) await events.fire('PlaceholderJump', [doc.bufnr, { charbefore: '', range: Range.create(0, 0, 0, 0) }]) await events.fire('PlaceholderJump', [doc.bufnr + 1, { charbefore: '(', range: Range.create(0, 0, 0, 0) }]) expect(called).toBe(0) await nvim.input('ifoo(b)') await events.fire('PlaceholderJump', [doc.bufnr, { charbefore: '(', range: Range.create(0, 5, 0, 6) }]) expect(called).toBe(1) }) it('should cancel trigger on InsertLeave', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: async (_doc, _position, token) => { return new Promise(resolve => { let timer = setTimeout(() => { resolve({ signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null }) }, 1000) token.onCancellationRequested(() => { clearTimeout(timer) resolve(undefined) }) }) } }, ['(', ','])) await helper.createDocument() await nvim.input('foo') let p = signature.triggerSignatureHelp() await helper.wait(10) await nvim.command('stopinsert') await nvim.call('feedkeys', [String.fromCharCode(27), 'in']) let res = await p expect(res).toBe(false) }) it('should not close signature on type', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } }, ['( ,'])) let doc = await helper.createDocument() await nvim.input('foo(') await doc.synchronize() await nvim.input('bar') await doc.synchronize() await helper.waitFloat() let win = await helper.getFloat() let lines = await helper.getWinLines(win.id) expect(lines[2]).toMatch('my signature') }) it('should close signature float when empty signatures returned', async () => { let empty = false disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { if (empty) return undefined return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } }, ['(', ','])) await helper.createDocument() await nvim.input('foo(') await helper.wait(100) let win = await helper.getFloat() expect(win).toBeDefined() empty = true await signature.triggerSignatureHelp() await helper.wait(50) let res = await nvim.call('coc#float#valid', [win.id]) expect(res).toBe(0) }) it('should close float on cursor moved', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } }, ['(', ','])) const show = async () => { await helper.createDocument() await nvim.input('i') await nvim.call('append', [1, 'bar']) await nvim.input('(') await helper.waitValue(async () => { let win = await helper.getFloat() return win != null }, true) } await show() await nvim.call('cursor', [2, 1]) await helper.waitValue(async () => { let win = await helper.getFloat() return win == null }, true) await nvim.input('') await show() await nvim.input(')') await helper.waitValue(async () => { let win = await helper.getFloat() return win == null }, true) }) }) describe('float window', () => { it('should align signature window to top', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } }, ['(', ','])) await helper.createDocument() let buf = await nvim.buffer await buf.setLines(['', '', '', '', ''], { start: 0, end: -1, strictIndexing: true }) await nvim.call('cursor', [5, 1]) await nvim.input('foo(') await helper.wait(100) let win = await helper.getFloat() expect(win).toBeDefined() let lines = await helper.getWinLines(win.id) expect(lines[2]).toMatch('my signature') let res = await nvim.call('GetFloatCursorRelative', [win.id]) as any expect(res.row).toBeLessThan(0) }) it('should show parameter docs', async () => { disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo(a, b)', 'my signature', ParameterInformation.create('a', 'foo'), ParameterInformation.create([7, 8], 'bar'))], activeParameter: 1, activeSignature: null } } }, ['(', ','])) await helper.createDocument() let buf = await nvim.buffer await buf.setLines(['', '', '', '', ''], { start: 0, end: -1, strictIndexing: true }) await nvim.call('cursor', [5, 1]) await nvim.input('foo(a,') await helper.wait(100) let win = await helper.getFloat() expect(win).toBeDefined() let lines = await helper.getWinLines(win.id) expect(lines.join('\n')).toMatch('bar') }) }) describe('configurations', () => { let { configurations } = workspace afterEach(() => { configurations.updateMemoryConfig({ 'signature.target': 'float', 'signature.hideOnTextChange': false, 'signature.enable': true, 'signature.triggerSignatureWait': 500 }) }) it('should cancel signature on timeout', async () => { configurations.updateMemoryConfig({ 'signature.triggerSignatureWait': 50 }) disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { clearTimeout(timer) resolve(undefined) }) let timer = setTimeout(() => { resolve({ signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null }) }, 200) }) } }, ['(', ','])) await helper.createDocument() await signature.triggerSignatureHelp() let win = await helper.getFloat() expect(win).toBeUndefined() configurations.updateMemoryConfig({ 'signature.triggerSignatureWait': 100 }) }) it('should hide signature window on text change', async () => { configurations.updateMemoryConfig({ 'signature.hideOnTextChange': true }) disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { let s = SignatureInformation.create('foo()', 'my signature') s.parameters = undefined return { signatures: [s], activeParameter: 0, activeSignature: null } } }, ['(', ','])) await helper.createDocument() await nvim.input('ifoo(') let winid = await helper.waitFloat() await nvim.input('x') await helper.wait(100) let res = await nvim.call('coc#float#valid', [winid]) expect(res).toBe(0) configurations.updateMemoryConfig({ 'signature.hideOnTextChange': false }) }) it('should disable signature help trigger', async () => { configurations.updateMemoryConfig({ 'signature.enable': false }) disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo()', 'my signature')], activeParameter: null, activeSignature: null } } }, ['(', ','])) await helper.createDocument() await nvim.input('foo') await nvim.input('(') await helper.wait(30) let win = await helper.getFloat() expect(win).toBeUndefined() }) it('should echo simple signature help', async () => { let idx = 0 let activeSignature = null configurations.updateMemoryConfig({ 'signature.target': 'echo' }) disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], { provideSignatureHelp: (_doc, _position) => { return { signatures: [SignatureInformation.create('foo(a, b)', 'my signature', ParameterInformation.create('a', 'foo'), ParameterInformation.create([7, 8], 'bar')), SignatureInformation.create('a'.repeat(workspace.env.columns + 10)) ], activeParameter: idx, activeSignature } } }, [])) await helper.createDocument() await nvim.input('foo(') await signature.triggerSignatureHelp() let line = await helper.getCmdline() expect(line).toMatch('(a, b)') await nvim.input('a,') idx = 1 await signature.triggerSignatureHelp() line = await helper.getCmdline() expect(line).toMatch('foo(a, b)') activeSignature = 1 await signature.triggerSignatureHelp() line = await helper.getCmdline() expect(line).toMatch('aaaaaa') }) it('should echo signature without match', async () => { let signatureHelp = { signatures: [SignatureInformation.create('foo(a, b)', 'my signature', ParameterInformation.create('c', 'foo'), ParameterInformation.create([7, 8], 'bar')), SignatureInformation.create('a'.repeat(workspace.env.columns + 10)) ], activeParameter: 0, activeSignature: null } signature.echoSignature(signatureHelp) await helper.wait(20) let line = await helper.getCmdline() expect(line).toMatch('foo') signatureHelp.signatures[0].parameters = undefined signature.echoSignature(signatureHelp) }) }) }) ================================================ FILE: src/__tests__/handler/symbols.test.ts ================================================ import { Buffer, Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, Range, SymbolInformation, SymbolKind } from 'vscode-languageserver-protocol' import events from '../../events' import Symbols from '../../handler/symbols/index' import languages from '../../languages' import { asDocumentSymbolTree } from '../../provider/documentSymbolManager' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' import Parser from './parser' let nvim: Neovim let symbols: Symbols let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim symbols = helper.plugin.getHandler().symbols }) beforeEach(() => { disposables.push(languages.registerDocumentSymbolProvider([{ language: 'javascript' }], { provideDocumentSymbols: document => { let text = document.getText() let parser = new Parser(text, text.includes('detail')) let res = parser.parse() return Promise.resolve(res) } })) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) disposables = [] await helper.reset() }) describe('Parser', () => { it('should parse content', async () => { let code = `class myClass { fun1() { } }` let parser = new Parser(code) let res = parser.parse() expect(res.length).toBeGreaterThan(0) }) }) describe('symbols handler', () => { async function createBuffer(code: string): Promise { let doc = await workspace.document doc.setFiletype('javascript') await doc.buffer.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() return doc.buffer } describe('configuration', () => { it('should get configuration', async () => { let bufnr = await nvim.call('bufnr', ['%']) as number let functionUpdate = symbols.autoUpdate(bufnr) expect(functionUpdate).toBe(false) helper.updateConfiguration('coc.preferences.currentFunctionSymbolAutoUpdate', true) functionUpdate = symbols.autoUpdate(bufnr) expect(functionUpdate).toBe(true) }) it('should update symbols automatically', async () => { helper.updateConfiguration('coc.preferences.currentFunctionSymbolAutoUpdate', true) let code = `class myClass { fun1() { } }` let buf = await createBuffer(code) await events.fire('CursorMoved', [buf.id, [2, 8]]) await helper.waitFor('eval', ['get(b:,"coc_current_function","")'], 'fun1') await events.fire('CursorMoved', [buf.id, [1, 8]]) await helper.waitFor('eval', ['get(b:,"coc_current_function","")'], 'myClass') }) }) describe('documentSymbols', () => { it('should create document symbol tree', () => { let uri = 'lsp:/1' let symbols = [ SymbolInformation.create('root', SymbolKind.Function, Range.create(0, 0, 0, 10), uri), SymbolInformation.create('child', SymbolKind.Function, Range.create(0, 3, 0, 7), uri, 'root'), SymbolInformation.create('child', SymbolKind.Function, Range.create(0, 0, 0, 10), uri, 'root'), ] let res = asDocumentSymbolTree(symbols) expect(res.length).toBe(2) }) it('should get empty metadata when provider not found', async () => { disposeAll(disposables) let doc = await workspace.document let res = languages.getDocumentSymbolMetadata(doc.textDocument) expect(res).toBeNull() let symbols = await languages.getDocumentSymbol(doc.textDocument, CancellationToken.None) expect(symbols).toBeNull() }) it('should get symbols of current buffer', async () => { let code = `class detail { fun1() { } }` await createBuffer(code) let res = await helper.plugin.cocAction('documentSymbols') expect(res.length).toBe(2) expect(res[1].detail).toBeDefined() }) it('should get current function symbols', async () => { let code = `class myClass { fun1() { } fun2() { } } ` await createBuffer(code) await nvim.call('cursor', [3, 0]) let res = await helper.doAction('getCurrentFunctionSymbol') expect(res).toBe('fun1') await nvim.command('normal! G') res = await helper.doAction('getCurrentFunctionSymbol') expect(res).toBe('') }) it('should reset coc_current_function when symbols do not exist', async () => { let code = `class myClass { fun1() { } }` await createBuffer(code) await nvim.call('cursor', [3, 0]) let res = await helper.doAction('getCurrentFunctionSymbol') expect(res).toBe('fun1') await nvim.command('normal! ggdG') res = await symbols.getCurrentFunctionSymbol() expect(res).toBe('') }) it('should support SymbolInformation', async () => { disposables.push(languages.registerDocumentSymbolProvider(['*'], { provideDocumentSymbols: doc => { let s = SymbolInformation.create('root', SymbolKind.Function, Range.create(0, 0, 0, 10), doc.uri) s.deprecated = true return [ s, SymbolInformation.create('child', SymbolKind.Function, Range.create(0, 3, 0, 7), doc.uri, 'root'), SymbolInformation.create('child', SymbolKind.Function, Range.create(0, 0, 0, 10), doc.uri, 'root') ] } }, { label: 'test' })) await helper.createDocument() let res = await symbols.getDocumentSymbols() expect(res.length).toBe(3) expect(res[0].text).toBe('root') await nvim.command('edit +setl\\ buftype=nofile b') res = await symbols.getDocumentSymbols() expect(res).toBeUndefined() }) }) describe('selectSymbolRange', () => { it('should show warning when no symbols exist', async () => { disposables.push(languages.registerDocumentSymbolProvider(['*'], { provideDocumentSymbols: () => { return [] } })) await helper.createDocument() await nvim.call('cursor', [3, 0]) await symbols.selectSymbolRange(false, '', ['Function']) let msg = await helper.getCmdline() expect(msg).toMatch(/No symbols found/) }) it('should select symbol range at cursor position', async () => { let code = `class myClass { fun1() { } }` await createBuffer(code) await nvim.call('cursor', [3, 0]) await helper.doAction('selectSymbolRange', false, '', ['Function', 'Method']) let mode = await nvim.mode expect(mode.mode).toBe('v') await nvim.input('') let res = await window.getSelectedRange('v') expect(res).toEqual({ start: { line: 1, character: 6 }, end: { line: 2, character: 6 } }) }) it('should select inner range', async () => { let code = `class myClass { fun1() { let foo; } }` await createBuffer(code) await nvim.call('cursor', [3, 3]) await symbols.selectSymbolRange(true, '', ['Method']) let mode = await nvim.mode expect(mode.mode).toBe('v') await nvim.input('') let res = await window.getSelectedRange('v') expect(res).toEqual({ start: { line: 2, character: 8 }, end: { line: 2, character: 16 } }) }) it('should reset visualmode when selection not found', async () => { let code = `class myClass {}` await createBuffer(code) await nvim.call('cursor', [1, 1]) await nvim.command('normal! gg0v$') let mode = await nvim.mode expect(mode.mode).toBe('v') await nvim.input('') await symbols.selectSymbolRange(true, 'v', ['Method']) mode = await nvim.mode expect(mode.mode).toBe('v') }) it('should select symbol range from select range', async () => { let code = `class myClass { fun1() { } }` let buf = await createBuffer(code) await nvim.call('cursor', [2, 8]) await nvim.command('normal! viw') await nvim.input('') await helper.doAction('selectSymbolRange', false, 'v', ['Class']) let mode = await nvim.mode expect(mode.mode).toBe('v') let doc = workspace.getDocument(buf.id) await nvim.input('') let res = await window.getSelectedRange('v') expect(res).toEqual({ start: { line: 0, character: 0 }, end: { line: 3, character: 4 } }) }) }) describe('cancel', () => { it('should cancel symbols request on insert', async () => { let cancelled = false disposables.push(languages.registerDocumentSymbolProvider([{ language: 'text' }], { provideDocumentSymbols: (_doc, token) => { return new Promise(s => { token.onCancellationRequested(() => { if (timer) clearTimeout(timer) cancelled = true s(undefined) }) let timer = setTimeout(() => { s(undefined) }, 3000) }) } })) let doc = await helper.createDocument('t.txt') let p = symbols.getDocumentSymbols(doc.bufnr) setTimeout(async () => { await nvim.input('i') }, 500) await p expect(cancelled).toBe(true) }) }) describe('workspaceSymbols', () => { it('should get workspace symbols', async () => { disposables.push(languages.registerWorkspaceSymbolProvider({ provideWorkspaceSymbols: (_query, _token) => { return [SymbolInformation.create('far', SymbolKind.Class, Range.create(0, 0, 0, 0), '')] }, resolveWorkspaceSymbol: sym => { let res = Object.assign({}, sym) res.location.uri = 'test:///foo' return res } })) let fn: any = languages.registerWorkspaceSymbolProvider.bind(languages) disposables.push(fn('vim', { provideWorkspaceSymbols: (_query, _token) => { return null } })) let res = await symbols.getWorkspaceSymbols('a') expect(res.length).toBe(1) let resolved = await helper.doAction('resolveWorkspaceSymbol', res[0]) expect(resolved?.location?.uri).toBe('test:///foo') }) it('should return symbol when resolve failed', async () => { disposables.push(languages.registerWorkspaceSymbolProvider({ provideWorkspaceSymbols: (_query, _token) => { return [SymbolInformation.create('far', SymbolKind.Class, Range.create(0, 0, 0, 0), '')] } })) let res = await helper.doAction('getWorkspaceSymbols') let resolved = await symbols.resolveWorkspaceSymbol(res[0]) expect(resolved).toBeDefined() }) }) }) ================================================ FILE: src/__tests__/handler/typeHierarchy.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken, TypeHierarchyItem, Disposable, Range, SymbolKind, Position, SymbolTag } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import languages, { ProviderName } from '../../languages' import TypeHierarchyHandler from '../../handler/typeHierarchy' import { addChildren } from '../../tree/LocationsDataProvider' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let handler: TypeHierarchyHandler beforeAll(async () => { await helper.setup() nvim = helper.nvim handler = helper.plugin.getHandler().typeHierarchy }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) function createItem(name: string, kind?: SymbolKind, uri?: string, range?: Range): TypeHierarchyItem { range = range ?? Range.create(0, 0, 0, 3) return { name, kind: kind ?? SymbolKind.Function, uri: uri ?? 'file:///1', range, selectionRange: range, } } const position = Position.create(0, 0) const token = CancellationToken.None describe('TypeHierarchy', () => { describe('TypeHierarchyManager', () => { it('should return false when provider not exists', async () => { let doc = await workspace.document let res = languages.hasProvider(ProviderName.TypeHierarchy, doc.textDocument) expect(res).toBe(false) }) it('should return merged results', async () => { disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy: () => { return null }, provideTypeHierarchySubtypes: () => { return [] }, provideTypeHierarchySupertypes: () => { return [] } })) disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy: () => { return [createItem('a'), createItem('b')] }, provideTypeHierarchySubtypes: () => { return [] }, provideTypeHierarchySupertypes: () => { return [] } })) disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy: () => { return [createItem('b'), createItem('c')] }, provideTypeHierarchySubtypes: () => { return [] }, provideTypeHierarchySupertypes: () => { return [] } })) let doc = await workspace.document let res = await languages.prepareTypeHierarchy(doc.textDocument, position, token) expect(res.length).toBe(3) }) it('should return empty array when provider not found', async () => { let item = createItem('foo') let res: any res = await languages.provideTypeHierarchySupertypes(item, token) expect(res).toEqual([]) res = await languages.provideTypeHierarchySubtypes(item, token) expect(res).toEqual([]) }) it('should return subtypes and supertypes', async () => { disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy: () => { return [createItem('b')] }, provideTypeHierarchySubtypes: () => { return [createItem('c')] }, provideTypeHierarchySupertypes: () => { return [createItem('d')] } })) let doc = await workspace.document let res = await languages.prepareTypeHierarchy(doc.textDocument, position, token) let arr: any[] arr = await languages.provideTypeHierarchySubtypes(res[0], token) expect(arr.length).toBe(1) expect(arr[0].source).toBeDefined() arr = await languages.provideTypeHierarchySupertypes(res[0], token) expect(arr.length).toBe(1) expect(arr[0].source).toBeDefined() }) it('should not throw when prepareTypeHierarchy throws', async () => { disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy: () => { throw new Error('my error') }, provideTypeHierarchySubtypes: () => { return undefined }, provideTypeHierarchySupertypes: () => { return undefined } })) let doc = await workspace.document let res = await languages.prepareTypeHierarchy(doc.textDocument, position, token) expect(res).toEqual([]) }) it('should return empty supertypes and supertypes', async () => { disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy: () => { return [createItem('b')] }, provideTypeHierarchySubtypes: () => { return null }, provideTypeHierarchySupertypes: () => { return undefined } })) let doc = await workspace.document let res = await languages.prepareTypeHierarchy(doc.textDocument, position, token) let arr: any[] arr = await languages.provideTypeHierarchySubtypes(res[0], token) expect(arr).toEqual([]) arr = await languages.provideTypeHierarchySupertypes(res[0], token) expect(arr).toEqual([]) }) }) describe('TypeHierarchyHandler', () => { it('should add children', async () => { let item = createItem('foo') addChildren(item, undefined) expect(item['children']).toBeUndefined() addChildren(item, [], CancellationToken.Cancelled) expect(item['children']).toBeUndefined() }) it('should throw when provider not exist', async () => { let fn = async () => { await handler.showTypeHierarchyTree('supertypes') } await expect(fn()).rejects.toThrow(Error) }) it('should show warning when prepare return empty', async () => { disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy() { return null }, provideTypeHierarchySupertypes() { return [] }, provideTypeHierarchySubtypes() { return [] } })) let plugin = helper.plugin await plugin.cocAction('showSuperTypes') await nvim.command('echo ""') await plugin.cocAction('showSubTypes') let line = await helper.getCmdline() expect(line).toMatch('Unable') }) it('should invoke super types and sub types action', async () => { let doc = await workspace.document disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy() { return [createItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))] }, provideTypeHierarchySupertypes() { return undefined }, provideTypeHierarchySubtypes() { return undefined } })) await handler.showTypeHierarchyTree('supertypes') await helper.waitFor('getline', [2], '- c foo') await nvim.command('exe 2') await nvim.input('') await helper.waitPrompt() await nvim.input('4') await helper.waitFor('getline', [1], 'Sub types') await nvim.input('') await helper.waitPrompt() await nvim.input('3') await helper.waitFor('getline', [1], 'Super types') }) it('should render description and support default action', async () => { let doc = await workspace.document let bufnr = doc.bufnr await doc.buffer.setLines(['foo'], { start: 0, end: -1, strictIndexing: false }) let fsPath = await createTmpFile('foo\nbar\ncontent\n') let uri = URI.file(fsPath).toString() disposables.push(languages.registerTypeHierarchyProvider([{ language: '*' }], { prepareTypeHierarchy() { return [createItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))] }, provideTypeHierarchySupertypes() { let item = createItem('bar', SymbolKind.Class, uri, Range.create(1, 0, 1, 3)) item.detail = 'Detail' item.tags = [SymbolTag.Deprecated] return [item] }, provideTypeHierarchySubtypes() { return [] } })) await handler.showTypeHierarchyTree('supertypes') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual([ 'Super types', '- c foo', ' + c bar Detail' ]) await nvim.command('exe 3') await nvim.input('t') await helper.waitFor('getline', ['.'], ' - c bar Detail') await nvim.input('') await helper.waitFor('expand', ['%:p'], fsPath) let res = await nvim.call('coc#cursor#position') expect(res).toEqual([1, 0]) let matches = await nvim.call('getmatches') as any[] expect(matches.length).toBe(1) await nvim.command(`b ${bufnr}`) await helper.wait(50) matches = await nvim.call('getmatches') as any[] expect(matches.length).toBe(0) await nvim.command(`wincmd o`) }) }) }) ================================================ FILE: src/__tests__/handler/workspace.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable, Location, Range } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import commands from '../../commands' import events from '../../events' import extensions from '../../extension' import WorkspaceHandler from '../../handler/workspace' import languages from '../../languages' import snippetManager from '../../snippets/manager' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let handler: WorkspaceHandler let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim handler = helper.plugin.getHandler().workspace }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) }) describe('Workspace handler', () => { async function checkFloat(content: string) { let win = await helper.getFloat() expect(win).toBeDefined() let buf = await win.buffer let lines = await buf.lines expect(lines.join('\n')).toMatch(content) } describe('events', () => { it('should reset autocmds of extensions', async () => { workspace.registerAutocmd({ event: 'CursorHold', callback: () => {}, }) workspace.registerAutocmd({ event: 'CursorMoved', callback: () => {}, }) let obj = workspace.autocmds.autocmds.get(1) Object.assign(obj, { _extensiionName: 'test' }) let m = extensions.manager as any m._onDidUnloadExtension.fire('test') let map = workspace.autocmds.autocmds let arr = Array.from(map.keys()) expect(arr).toEqual([2]) let output = await nvim.call('execute', 'autocmd coc_dynamic_autocmd') as string expect(output).toMatch('CursorMoved') expect(output.includes('CursorHold')).toBe(false) nvim.command('autocmd! coc_dynamic_autocmd', true) }) }) describe('commands', () => { it('should check filetype', async () => { await helper.createDocument('t.vim') await commands.executeCommand('document.echoFiletype') let line = await helper.getCmdline() expect(line).toMatch('vim') }) it('should show workspace folders', async () => { await helper.edit(__filename) await commands.executeCommand('workspace.workspaceFolders') let line = await helper.getCmdline() expect(line).toMatch('coc.nvim') }) it('should write writeHeapSnapshot', async () => { const v8 = require('v8') let called = false let spy = jest.spyOn(v8, 'writeHeapSnapshot').mockImplementation(() => { called = true }) let filepath = await commands.executeCommand('workspace.writeHeapSnapshot') spy.mockRestore() expect(filepath).toBeDefined() expect(called).toBe(true) }) it('should show output', async () => { window.createOutputChannel('foo') window.createOutputChannel('bar') let p = commands.executeCommand('workspace.showOutput') await helper.waitFloat() await nvim.input('') await p let bufname = await nvim.call('bufname', ['%']) expect(bufname).toBe('') await commands.executeCommand('workspace.showOutput', 'foo') bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch('output') }) it('should open location', async () => { let winid = await nvim.call('win_getid') await commands.executeCommand('workspace.openLocation', winid, Location.create('lsp:/1', Range.create(0, 0, 0, 0))) let bufname = await nvim.call('bufname', ['%']) expect(bufname).toBe('lsp:/1') }) it('should clear watchman roots', async () => { let success = true let spy = jest.spyOn(window, 'runTerminalCommand').mockImplementation(() => { return Promise.resolve({ success, bufnr: 1 }) }) let res = await commands.executeCommand('workspace.clearWatchman') expect(res).toBe(true) success = false res = await commands.executeCommand('workspace.clearWatchman') expect(res).toBe(false) spy.mockRestore() }) }) describe('methods', () => { it('should rename buffer', async () => { let doc = await helper.createDocument('a') let fsPath = URI.parse(doc.uri).fsPath.replace(/a$/, 'b') disposables.push(Disposable.create(() => { if (fs.existsSync(fsPath)) fs.unlinkSync(fsPath) })) let p = handler.renameCurrent() await helper.waitValue(() => nvim.call('mode'), 'c') await nvim.input('b') await p let name = await nvim.eval('bufname("%")') as string expect(name.endsWith('b')).toBe(true) p = handler.renameCurrent() await helper.waitValue(() => nvim.call('mode'), 'c') await nvim.input('') await p }) it('should rename file', async () => { let dir = path.join(os.tmpdir(), uuid()) fs.mkdirSync(dir, { recursive: true }) let fsPath = path.join(dir, 'x') let newPath = path.join(dir, 'b') disposables.push(Disposable.create(() => { fs.rmSync(dir, { recursive: true, force: true }) })) fs.writeFileSync(newPath, '', 'utf8') fs.writeFileSync(fsPath, 'foo', 'utf8') await helper.createDocument(fsPath) let spy = jest.spyOn(window, 'showPrompt').mockImplementation(() => { return Promise.resolve(true) }) let p = commands.executeCommand('workspace.renameCurrentFile') await helper.waitFor('mode', [], 'c') await nvim.input('b') await p spy.mockRestore() let name = await nvim.eval('bufname("%")') as string expect(name.endsWith('b')).toBe(true) expect(fs.existsSync(newPath)).toBe(true) let content = fs.readFileSync(newPath, 'utf8') expect(content).toMatch(/foo/) }) it('should not rename when reject overwrite', async () => { let dir = path.join(os.tmpdir(), uuid()) fs.mkdirSync(dir, { recursive: true }) let fsPath = path.join(dir, 'x') let newPath = path.join(dir, 'b') disposables.push(Disposable.create(() => { fs.rmSync(dir, { recursive: true, force: true }) })) fs.writeFileSync(newPath, '', 'utf8') await helper.createDocument(fsPath) let spy = jest.spyOn(window, 'showPrompt').mockImplementation(() => { return Promise.resolve(false) }) let p = handler.renameCurrent() await helper.waitFor('mode', [], 'c') await nvim.input('b') await p spy.mockRestore() let bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch(/x$/) }) it('should open local config', async () => { let dir = path.join(os.tmpdir(), '.vim') fs.rmSync(dir, { recursive: true, force: true }) fs.mkdirSync(path.join(os.tmpdir(), '.git'), { recursive: true }) await helper.edit(path.join(os.tmpdir(), 't')) let root = workspace.root expect(root).toBe(os.tmpdir()) let p = handler.openLocalConfig() await helper.waitPromptWin() await nvim.input('n') await p p = handler.openLocalConfig() await helper.waitPromptWin() await nvim.input('y') await p let bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch('coc-settings.json') }) it('should not throw when workspace folder does not exist', async () => { helper.updateConfiguration('workspace.rootPatterns', [], disposables) helper.updateConfiguration('workspace.ignoredFiletypes', ['vim'], disposables) await nvim.command('enew') await (window as any).openLocalConfig() await nvim.command(`e ${path.join(os.tmpdir(), 'a')}`) await helper.doAction('openLocalConfig') await nvim.command(`e t.md`) await nvim.command('setf markdown') await handler.openLocalConfig() await nvim.command(`e ${path.join(os.tmpdir(), 't.vim')}`) await nvim.command('setf vim') let called = false let spy = jest.spyOn(window, 'showWarningMessage').mockImplementation(() => { called = true return Promise.resolve(undefined) }) await commands.executeCommand('workspace.openLocalConfig') expect(called).toBe(true) spy.mockRestore() }) it('should add workspace folder', async () => { expect(() => { handler.addWorkspaceFolder(undefined) }).toThrow(TypeError) expect(() => { handler.addWorkspaceFolder(__filename) }).toThrow(Error) await helper.plugin.cocAction('addWorkspaceFolder', __dirname) let folders = workspace.workspaceFolderControl.workspaceFolders let uri = URI.file(__dirname).toString() let find = folders.find(o => o.uri === uri) expect(find).toBeDefined() }) it('should remove workspace folder', async () => { expect(() => { handler.addWorkspaceFolder(__filename) }).toThrow(Error) expect(() => { handler.addWorkspaceFolder(__filename) }).toThrow(Error) await helper.plugin.cocAction('addWorkspaceFolder', __dirname) await helper.plugin.cocAction('removeWorkspaceFolder', __dirname) let folders = workspace.workspaceFolderControl.workspaceFolders let uri = URI.file(__dirname).toString() let find = folders.find(o => o.uri === uri) expect(find).toBeUndefined() }) it('should check env on vim resized', async () => { await events.fire('VimResized', [80, 80]) expect(workspace.env.columns).toBe(80) await events.fire('VimResized', [160, 80]) expect(workspace.env.columns).toBe(160) }) it('should should error message for document not attached', async () => { disposables.push(languages.registerDocumentFormatProvider(['*'], { provideDocumentFormattingEdits: () => { return [] } })) await handler.bufferCheck() await checkFloat('Provider state') await nvim.call('coc#float#close_all', []) await nvim.command('edit t|let b:coc_enabled = 0') await commands.executeCommand('document.checkBuffer') await checkFloat('not attached') await nvim.call('coc#float#close_all', []) await nvim.command('edit +setl\\ buftype=nofile b') await helper.doAction('bufferCheck') await checkFloat('not attached') await nvim.call('coc#float#close_all', []) helper.updateConfiguration('coc.preferences.maxFileSize', '1KB') await helper.edit(__filename) await handler.bufferCheck() await checkFloat('not attached') await nvim.call('coc#float#close_all', []) }) it('should check json extension', async () => { let spy = jest.spyOn(extensions, 'has').mockImplementation(() => { return true }) await helper.doAction('checkJsonExtension') spy.mockRestore() await helper.doAction('checkJsonExtension') let line = await helper.getCmdline() expect(line).toBeDefined() }) it('should get rootPatterns', async () => { let bufnr = await nvim.call('bufnr', ['%']) let res = await helper.doAction('rootPatterns', bufnr) expect(res).toBeDefined() }) it('should get config by key', async () => { let res = await helper.doAction('getConfig', ['suggest']) expect(res.autoTrigger).toBeDefined() }) it('should open log', async () => { await helper.doAction('openLog') let bufname = await nvim.call('bufname', ['%']) as string expect(bufname).toMatch('coc-nvim') }) it('should get configuration of current document', async () => { let config = await handler.getConfiguration('suggest') let wait = config.get('triggerCompletionWait') expect(wait).toBe(0) }) it('should get root patterns', async () => { let doc = await helper.createDocument() let patterns = handler.getRootPatterns(doc.bufnr) expect(patterns).toBeDefined() patterns = handler.getRootPatterns(999) expect(patterns).toBeNull() }) }) describe('doKeymap()', () => { it('should return default value when key mapping does not exist', async () => { let res = await helper.doAction('doKeymap', ['not_exists', '']) expect(res).toBe('') }) it('should support repeat key mapping', async () => { let called = false await nvim.command('nmap do (coc-test)') disposables.push(workspace.registerKeymap(['n'], 'test', () => { called = true })) await helper.waitValue(async () => { let res = await nvim.call('maparg', ['(coc-test)', 'n']) as string return res.length > 0 }, true) await nvim.call('feedkeys', ['do', 'i']) await helper.waitValue(() => { return called }, true) }) }) describe('snippetCheck()', () => { it('should return false when coc-snippets not found', async () => { let fn = async () => { expect(await handler.snippetCheck(true, false)).toBe(false) } await expect(fn()).rejects.toThrow(Error) let spy = jest.spyOn(extensions.manager, 'call').mockImplementation(() => { return Promise.resolve(true) }) expect(await handler.snippetCheck(true, false)).toBe(true) spy.mockRestore() }) it('should check jump', async () => { expect(await handler.snippetCheck(false, true)).toBe(false) let spy = jest.spyOn(snippetManager, 'jumpable').mockImplementation(() => { return true }) expect(await handler.snippetCheck(false, true)).toBe(true) spy.mockRestore() }) }) }) ================================================ FILE: src/__tests__/helper.ts ================================================ import type { Buffer, Neovim, Window } from '@chemzqm/neovim' import * as cp from 'child_process' import crypto from 'crypto' import { EventEmitter } from 'events' import fs from 'fs' import net, { Server } from 'net' import os from 'os' import path from 'path' import util from 'util' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import attach from '../attach' import type { Completion } from '../completion' import { DurationCompleteItem } from '../completion/types' import events from '../events' import type Document from '../model/document' import type Plugin from '../plugin' import type { ProviderResult } from '../provider' import { OutputChannel } from '../types' import { equals } from '../util/object' import { terminate } from '../util/processes' import type { Workspace } from '../workspace' const vimrc = path.resolve(__dirname, 'vimrc') export interface CursorPosition { bufnum: number lnum: number col: number } const nullChannel: OutputChannel = { content: '', show: () => {}, dispose: () => {}, name: 'null', append: () => {}, appendLine: () => {}, clear: () => {}, hide: () => {} } process.on('uncaughtException', err => { let msg = 'Uncaught exception: ' + err.stack console.error(msg) }) export class Helper extends EventEmitter { public proc: cp.ChildProcess private server: Server public plugin: Plugin public reportError = true constructor() { super() this.setMaxListeners(99) } public get workspace(): Workspace { if (!this.plugin || !this.plugin.workspace) throw new Error('helper not attached') return this.plugin.workspace } public get completion(): Completion { if (!this.plugin || !this.plugin.completion) throw new Error('helper not attached') return this.plugin.completion } public get nvim(): Neovim { return this.plugin.nvim } public async setup(init = true): Promise { let proc = this.proc = cp.spawn(process.env.NVIM_COMMAND ?? 'nvim', ['-u', vimrc, '-i', 'NONE', '--embed'], { cwd: __dirname }) proc.unref() let plugin = this.plugin = attach({ proc }) await this.nvim.uiAttach(160, 80, {}) this.nvim.call('coc#rpc#set_channel', [1], true) this.nvim.on('vim_error', err => { if (typeof err === 'string' && err.startsWith('Lua')) { console.error('Error from vim: ', err) } }) this.nvim.on('notification', async (method, args) => { if (method == 'Log') { // console.log(args) } }) if (init) await plugin.init('') return plugin } public async setupVim(): Promise { if (process.env.VIM_NODE_RPC != '1') { throw new Error(`VIM_NODE_RPC should be 1`) } let server let promise = new Promise(resolve => { server = this.server = net.createServer(socket => { this.plugin = attach({ reader: socket, writer: socket }) this.nvim.on('vim_error', err => { if (this.reportError) console.error('Error from vim: ', err) }) resolve() }) }) let address = await this.listenOnVim(server) let proc = this.proc = cp.spawn(process.env.VIM_COMMAND ?? 'vim', ['--clean', '--not-a-term', '-u', vimrc], { stdio: 'pipe', shell: true, cwd: __dirname, env: { COC_NVIM_REMOTE_ADDRESS: address, ...process.env } }) proc.on('error', err => { console.error(err) }) proc.on('exit', code => { if (code) console.error('vim exit with code ' + code) }) await promise await this.plugin.init('') } private async listenOnVim(server: Server): Promise { const isWindows = process.platform === 'win32' return new Promise((resolve, reject) => { if (!isWindows) { // not work on old version vim. const socket = path.join(os.tmpdir(), `coc-test-${uuid()}.sock`) server.listen(socket, () => { resolve(socket) }) server.on('error', reject) server.unref() } else { getPort().then(port => { let localhost = '127.0.0.1' server.listen(port, localhost, () => { resolve(`${localhost}:${port}`) }) server.on('error', reject) }, reject) } server.unref() }) } public async reset(): Promise { let mode = await this.nvim.mode if (mode.blocking && mode.mode == 'r') { await this.nvim.input('') } else if (mode.mode != 'n' || mode.blocking) { await this.nvim.call('feedkeys', [String.fromCharCode(27), 'in']) } this.completion.cancelAndClose() this.workspace.reset() this.nvim.call('coc#float#close_all', [], true) await this.nvim.command('silent! %bwipeout! | setl nopreviewwindow') await this.workspace.document } public async shutdown(): Promise { if (this.plugin) this.plugin.dispose() if (this.nvim) await this.nvim.quit() if (this.server) this.server.close() if (this.proc) terminate(this.proc) if (typeof global.gc === 'function') { global.gc() } } public wait(ms = 30): Promise { return new Promise(resolve => { setTimeout(() => { resolve() }, ms) }) } public async waitPrompt(): Promise { for (let i = 0; i < 60; i++) { await this.wait(30) let prompt = await this.nvim.call('coc#prompt#activated') if (prompt) return } throw new Error('Wait prompt timeout after 2s') } public async waitPromptWin(): Promise { for (let i = 0; i < 60; i++) { await this.wait(30) let winid = await this.nvim.call('coc#dialog#get_prompt_win') as number if (winid != -1) return winid } throw new Error('Wait prompt window timeout after 2s') } public async waitFloat(): Promise { for (let i = 0; i < 50; i++) { await this.wait(20) let winid = await this.nvim.call('GetFloatWin') as number if (winid) return winid } throw new Error('timeout after 2s') } public async doAction(method: string, ...args: any[]): Promise { return await this.plugin.cocAction(method, ...args) } public async items(): Promise { return this.completion?.activeItems.slice() } public async waitPopup(): Promise { let visible = await this.nvim.call('coc#pum#visible') if (visible) return let res = await events.race(['MenuPopupChanged'], 8000) if (!res) throw new Error('wait pum timeout after 8s') } public async confirmCompletion(idx: number): Promise { await this.nvim.call('coc#pum#select', [idx, 1, 1]) } public async visible(word: string, source?: string): Promise { await this.waitPopup() let items = this.completion.activeItems if (!items) return false let item = items.find(o => o.word == word) if (!item) return false if (source && item.source.name != source) return false return true } public async edit(file?: string): Promise { if (!file || !path.isAbsolute(file)) { file = path.join(__dirname, file ? file : `${uuid()}`) } let escaped = await this.nvim.call('fnameescape', file) as string await this.nvim.command(`edit ${escaped}`) let doc = await this.workspace.document return doc.buffer } public async createDocument(name?: string): Promise { let buf = await this.edit(name) let doc = this.workspace.getDocument(buf.id) if (!doc) return await this.workspace.document return doc } public async listInput(input: string): Promise { await events.fire('InputChar', ['list', input, 0]) } public async getCmdline(lnum?: number): Promise { let str = '' let n = await this.nvim.eval('&lines') as number for (let i = 1, l = 70; i < l; i++) { let ch = await this.nvim.call('screenchar', [lnum ?? n - 1, i]) as number if (ch == -1) break str += String.fromCharCode(ch) } return str.trim() } public updateConfiguration(key: string, value: any, disposables?: Disposable[]): () => void { let curr = this.workspace.getConfiguration(key) let { configurations } = this.workspace configurations.updateMemoryConfig({ [key]: value }) let fn = () => { configurations.updateMemoryConfig({ [key]: curr }) } if (disposables) disposables.push(Disposable.create(fn)) return fn } public async getMatches(hlGroup: string): Promise { let res = await this.nvim.call('getmatches') as any[] let list = [] res.forEach(o => { if (o.group === hlGroup) { for (const [key, value] of Object.entries(o)) { if (key.startsWith('pos')) { list.push(value) } } } }) return list } public async mockFunction(name: string, result: string | number | any): Promise { let content = ` function! ${name}(...) return ${typeof result == 'number' ? result : JSON.stringify(result)} endfunction` await this.nvim.exec(content) } public async getFloat(kind?: string): Promise { if (!kind) { let ids = await this.nvim.call('coc#float#get_float_win_list') as number[] return ids.length ? this.nvim.createWindow(ids[0]) : undefined } else { let id = await this.nvim.call('coc#float#get_float_by_kind', [kind]) as number return id ? this.nvim.createWindow(id) : undefined } } public async getWinLines(winid: number): Promise { return await this.nvim.eval(`getbufline(winbufnr(${winid}), 1, '$')`) as string[] } public async waitFor(method: string, args: any[], value: T): Promise { let find = false let res for (let i = 0; i < 100; i++) { await this.wait(20) res = await this.nvim.call(method, args) as T if (equals(res, value) || (value instanceof RegExp && value.test(res.toString()))) { find = true break } } if (!find) { throw new Error(`waitFor ${value} timeout, current: ${res}`) } } public async waitNotification(event: string): Promise { return new Promise((resolve, reject) => { let fn = (method: string) => { if (method == event) { clearTimeout(timer) this.nvim.removeListener('notification', fn) resolve() } } let timer = setTimeout(() => { this.nvim.removeListener('notification', fn) reject(new Error('wait notification timeout after 2s')) }, 2000) this.nvim.on('notification', fn) }) } public async waitValue(fn: () => ProviderResult, value: T): Promise { let find = false for (let i = 0; i < 200; i++) { await this.wait(20) let res = await Promise.resolve(fn()) if (equals(res, value)) { find = true break } } if (!find) { throw new Error(`waitValue ${value} timeout`) } } public createNullChannel(): OutputChannel { return nullChannel } public generateRandomHash(algorithm = 'sha256') { const randomString = Math.random().toString(36).substring(2) // 生成随机字符串 const hash = crypto.createHash(algorithm) .update(randomString) .digest('hex') // 输出十六进制格式 return hash } } export async function createTmpFile(content: string, disposables?: Disposable[]): Promise { let tmpFolder = path.join(os.tmpdir(), `coc-${process.pid}`) if (!fs.existsSync(tmpFolder)) { fs.mkdirSync(tmpFolder) } let fsPath = path.join(tmpFolder, uuid()) await util.promisify(fs.writeFile)(fsPath, content, 'utf8') if (disposables) { disposables.push(Disposable.create(() => { if (fs.existsSync(fsPath)) fs.unlinkSync(fsPath) })) } return fsPath } export function makeLine(length) { let result = '' let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 (){};,\\<>+=`^*!@#$%[]:"/?' let charactersLength = characters.length for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)) } return result } let currPort = 5000 export function getPort(): Promise { let port = currPort let fn = cb => { let server = net.createServer() server.listen(port, () => { server.once('close', () => { currPort = port + 1 cb(port) }) server.close() }) server.on('error', () => { port += 1 fn(cb) }) } return new Promise(resolve => { fn(resolve) }) } export default new Helper() ================================================ FILE: src/__tests__/list/commandTask.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import path from 'path' import { ListContext, ListTask } from '../../list/types' import manager from '../../list/manager' import helper, { createTmpFile } from '../helper' import BasicList from '../../list/basic' import { Disposable } from 'vscode-languageserver-protocol' import { disposeAll } from '../../util' class DataList extends BasicList { public name = 'data' public async loadItems(_context: ListContext): Promise { let fsPath = await createTmpFile(`console.log('foo');console.log('');console.log('bar');`) return this.createCommandTask({ cmd: 'node', args: [fsPath], cwd: path.dirname(fsPath), onLine: line => { if (!line) return undefined return { label: line } } }) } } class SleepList extends BasicList { public name = 'sleep' public loadItems(_context: ListContext): Promise { return Promise.resolve(this.createCommandTask({ cmd: 'sleep', args: ['10'], onLine: line => { return { label: line } } })) } } class StderrList extends BasicList { public name = 'stderr' public async loadItems(_context: ListContext): Promise { let fsPath = await createTmpFile(`console.error('stderr');console.log('stdout')`) return Promise.resolve(this.createCommandTask({ cmd: 'node', args: [fsPath], cwd: path.dirname(fsPath), onLine: line => { return { label: line } } })) } } class ErrorTask extends BasicList { public name = 'error' public async loadItems(_context: ListContext): Promise { return Promise.resolve(this.createCommandTask({ cmd: 'NOT_EXISTS', args: [], cwd: __dirname, onLine: line => { return { label: line } } })) } } let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) manager.reset() await helper.reset() }) describe('Command task', () => { it('should not show stderr', async () => { disposables.push(manager.registerList(new StderrList())) await manager.start(['stderr']) await manager.session.ui.ready let lines = await nvim.call('getline', [1, '$']) as string[] expect(lines).toEqual(['stdout']) }) it('should not show error', async () => { disposables.push(manager.registerList(new ErrorTask())) await manager.start(['error']) await helper.wait(300) await nvim.command('redraw') let len = manager.session.ui.length expect(len).toBe(0) }) it('should create command task', async () => { let list = new DataList() disposables.push(manager.registerList(list)) await manager.start(['data']) await manager.session.ui.ready await helper.wait(100) let lines = await nvim.call('getline', [1, '$']) as string[] expect(lines).toEqual(['foo', 'bar']) }) it('should stop command task', async () => { let list = new SleepList() disposables.push(manager.registerList(list)) await manager.start(['sleep']) manager.session.stop() }) }) ================================================ FILE: src/__tests__/list/history.test.ts ================================================ import History from '../../list/history' import { DataBase } from '../../list/db' import os from 'os' import fs from 'fs' import path from 'path' import { v4 as uuid } from 'uuid' function createTmpDir(): string { let dir = path.join(os.tmpdir(), uuid()) fs.mkdirSync(dir, { recursive: true }) return dir } afterEach(() => { let DB_PATH = path.join(process.env.COC_DATA_HOME, 'list_history.dat') if (fs.existsSync(DB_PATH)) { fs.unlinkSync(DB_PATH) } }) describe('History', () => { it('should migrate history.json', async () => { let dir = createTmpDir() History.migrate(dir) History.migrate(path.join(os.tmpdir(), 'not_exists')) dir = createTmpDir() let file = path.join(dir, 'list-a-history.json') fs.writeFileSync(file, '{"x": 1}') History.migrate(dir) dir = createTmpDir() file = path.join(dir, 'list-mrn-history.json') let obj = { 'L1VzZXJzL2NoZW16cW0vdmltLWRldi9jb2MubnZpbQ==': ['list'] } fs.writeFileSync(file, JSON.stringify(obj, null, 2)) History.migrate(dir) }) it('should filter history', async () => { let db = new DataBase() db.save() db.addItem('name', 'text', '/a/b') let p = { input: '' } let history = new History(p, 'name', db, '/a/b') history.filter() expect(history.filtered).toEqual(['text']) p.input = 't' history.filter() expect(history.filtered).toEqual(['text']) history.previous() history.filter() expect(history.filtered).toEqual(['text']) }) it('should add item', async () => { let db = new DataBase() let p = { input: '' } let history = new History(p, 'name', db, '/a/b') history.add() p.input = 'input' history.add() p.input = '' history.filter() expect(history.filtered).toEqual(['input']) }) it('should change to previous', async () => { let db = new DataBase() let p = { input: '' } let history = new History(p, 'name', db, '/a/b') history.previous() db.addItem('name', 'one', '/a/b') db.addItem('name', 'two', '/a/b') db.addItem('name', 'three', '/a/b/c') history.filter() history.previous() history.previous() expect(history.index).toBe(0) expect(history.curr).toBe('one') }) it('should change to next', async () => { let db = new DataBase() let p = { input: '' } let history = new History(p, 'name', db, '/a/b') history.next() db.addItem('name', 'one', '/a/b') db.addItem('name', 'two', '/a/b') db.addItem('name', 'three', '/a/b/c') history.filter() history.next() history.next() history.next() expect(history.index).toBe(0) expect(history.curr).toBe('one') }) }) describe('DataBase', () => { it('should not throw on load', async () => { let spy = jest.spyOn(DataBase.prototype, 'load').mockImplementation(() => { throw new Error('error') }) new DataBase() spy.mockRestore() }) it('should add items', async () => { let db = new DataBase() db.addItem('name', 'x'.repeat(260), '/a/b/c') let item = db.currItems[0] expect(item[0].length).toBe(255) db.addItem('name', 'xy', '/a/b/c') db.addItem('name', 'xy', '/a/b/c') expect(db.currItems.length).toBe(2) }) it('should save data', async () => { let db = new DataBase() db.addItem('name', 'text', '/a/b/c') db.addItem('other_name', 'te', '/a/b/x/y') db.save() let d = new DataBase() expect(d.currItems.length).toBe(2) }) }) ================================================ FILE: src/__tests__/list/manager.test.ts ================================================ import { Neovim, Window } from '@chemzqm/neovim' import EventEmitter from 'events' import path from 'path' import { Range } from 'vscode-languageserver-types' import events from '../../events' import manager, { createConfigurationNode, ListManager } from '../../list/manager' import { IList } from '../../list/types' import { QuickfixItem } from '../../types' import { toArray } from '../../util/array' import { CancellationError } from '../../util/errors' import window from '../../window' import helper from '../helper' let nvim: Neovim const locations: ReadonlyArray = [{ filename: __filename, col: 2, lnum: 1, text: 'foo' }, { filename: __filename, col: 1, lnum: 2, text: 'Bar' }, { filename: __filename, col: 1, lnum: 3, text: 'option' }] async function getFloats(): Promise { let ids = await nvim.call('coc#float#get_float_win_list', []) as number[] if (!ids) return [] return ids.map(id => nvim.createWindow(id)) } beforeAll(async () => { await helper.setup() nvim = helper.nvim await nvim.setVar('coc_jump_locations', locations) }) afterEach(async () => { manager.reset() await helper.reset() }) afterAll(async () => { await helper.shutdown() }) describe('list', () => { describe('createConfigurationNode', () => { it('should createConfigurationNode', async () => { expect(createConfigurationNode('foo', true)).toBeDefined() expect(createConfigurationNode('bar', false)).toBeDefined() expect(createConfigurationNode('foo', false, 'id')).toBeDefined() }) }) describe('events', () => { it('should cancel and enable prompt', async () => { let winid = await nvim.call('win_getid') await manager.start(['location']) await manager.session.ui.ready await nvim.call('win_gotoid', [winid]) await helper.waitValue(async () => { return await nvim.call('coc#prompt#activated') }, 0) await nvim.command('wincmd p') await helper.waitPrompt() }) }) describe('list commands', () => { it('should not quit list with --no-quit', async () => { let list: IList = { name: 'test', actions: [{ name: 'open', execute: _item => { // noop } }], defaultAction: 'open', loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'bar' }]), resolveItem: item => { item.label = item.label.slice(0, 1) return Promise.resolve(item) } } global.__TEST__ = false let disposable = manager.registerList(list) global.__TEST__ = true await manager.start(['--normal', '--no-quit', 'test']) await manager.session.ui.ready let id = await nvim.eval('win_getid()') as number await manager.doAction() disposable.dispose() let wins = await nvim.windows let ids = wins.map(o => o.id) expect(ids).toContain(id) }) it('should do default action for first item', async () => { expect(ListManager).toBeDefined() await manager.start(['--normal', '--first', 'location']) let filename = path.basename(__filename) await helper.waitValue(async () => { let name = await nvim.eval('bufname("%")') as string return name.includes(filename) }, true) let pos = await nvim.eval('getcurpos()') expect(pos[1]).toBe(1) expect(pos[2]).toBe(2) }) it('should goto next & previous', async () => { await manager.start(['location']) await manager.session?.ui.ready await helper.waitPrompt() await manager.session?.ui.ready await manager.doAction() await helper.doAction('listCancel') let bufname = await nvim.eval('expand("%:p")') expect(bufname).toMatch('manager.test.ts') await helper.doAction('listNext') let line = await nvim.call('line', '.') expect(line).toBe(2) await helper.doAction('listPrev') line = await nvim.call('line', '.') expect(line).toBe(1) }) it('should parse arguments', async () => { await manager.start(['--input=test', '--reverse', '--normal', '--no-sort', '--ignore-case', '--top', '--number-select', '--auto-preview', '--strict', 'location']) await manager.session?.ui.ready let opts = manager.session?.listOptions expect(opts).toEqual({ reverse: true, numberSelect: true, autoPreview: true, first: false, input: 'test', interactive: false, matcher: 'strict', ignorecase: true, position: 'top', mode: 'normal', noQuit: false, sort: false }) }) }) describe('list configuration', () => { it('should change indicator', async () => { helper.updateConfiguration('list.indicator', '>>') manager.prompt.input = 'foo' await manager.start(['location']) await manager.session.ui.ready await helper.waitValue(async () => { let line = await helper.getCmdline() return line.includes('>>') }, true) await events.fire('FocusGained', []) }) it('should split right for preview window', async () => { helper.updateConfiguration('list.previewSplitRight', true) await manager.doAction('preview') await manager.resume() let win = await nvim.window await manager.start(['location']) await manager.session?.ui.ready await manager.doAction('preview') await helper.waitValue(async () => { let wins = await nvim.windows return wins.length }, 3) manager.prompt.cancel() await nvim.call('win_gotoid', [win.id]) await nvim.command('wincmd l') let curr = await nvim.window let isPreview = await curr.getVar('previewwindow') expect(isPreview).toBe(1) }) it('should use smartcase for strict match', async () => { helper.updateConfiguration('list.smartCase', true) await manager.start(['--input=Man', '--strict', 'location']) await manager.session?.ui.ready let items = await manager.session?.ui.getItems() expect(items.length).toBe(0) }) it('should use smartcase for fuzzy match', async () => { helper.updateConfiguration('list.smartCase', true) await manager.start(['--input=Man', 'location']) await manager.session?.ui.ready let items = await manager.session?.ui.getItems() expect(items.length).toBe(0) }) it('should toggle selection mode', async () => { await manager.start(['--normal', 'location']) await manager.session?.ui.ready await helper.waitPrompt() await window.selectRange(Range.create(0, 0, 3, 0)) await manager.session?.ui.toggleSelection() let items = await manager.session?.ui.getItems() expect(items.length).toBeGreaterThan(0) }) it('should change next and previous keymap', async () => { helper.updateConfiguration('list.nextKeymap', '') helper.updateConfiguration('list.previousKeymap', '') await manager.start(['location']) await manager.session.ui.ready await helper.waitPrompt() await nvim.eval('feedkeys("\\", "in")') await helper.waitValue(async () => { let line = await nvim.line return line.includes('Bar') }, true) await nvim.eval('feedkeys("\\", "in")') await helper.waitValue(async () => { let line = await nvim.line return line.includes('foo') }, true) }) it('should respect mouse events', async () => { async function setMouseEvent(line: number): Promise { let winid = manager.session?.ui.winid await nvim.command(`let v:mouse_winid = ${winid}`) await nvim.command(`let v:mouse_lnum = ${line}`) await nvim.command(`let v:mouse_col = 1`) } await manager.start(['--normal', 'location']) await manager.session.ui.ready await setMouseEvent(1) await manager.onNormalInput('') await setMouseEvent(2) await manager.onNormalInput('') await setMouseEvent(3) await manager.onNormalInput('') await helper.waitValue(async () => { let items = await manager.session?.ui.getItems() return items.length }, 3) }) it('should toggle preview', async () => { helper.updateConfiguration('list.floatPreview', true) await manager.start(['--normal', '--auto-preview', 'location']) await manager.session.ui.ready await helper.waitValue(async () => { let wins = await getFloats() return wins.length > 0 }, true) await manager.togglePreview() await helper.waitValue(async () => { let wins = await getFloats() return wins.length > 0 }, false) await manager.togglePreview() manager.session.ui.setCursor(2) await helper.waitValue(async () => { let wins = await getFloats() return wins.length > 0 }, true) }) it('should show help of current list', async () => { await manager.start(['--normal', '--auto-preview', 'location']) await manager.session.ui.ready await manager.session?.showHelp() let bufname = await nvim.call('bufname', '%') expect(bufname).toBe('[LIST HELP]') }) it('should resolve list item', async () => { let list: IList = { name: 'test', actions: [{ name: 'open', execute: _item => { // noop } }], defaultAction: 'open', loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'foo bar' }]), resolveItem: item => { item.label = 'foo bar' return Promise.resolve(item) } } let disposable = manager.registerList(list, true) await manager.start(['--normal', 'test']) await manager.session.ui.ready await helper.waitFor('getline', ['.'], 'foo bar') await manager.session.next() await manager.session.resolveItem() disposable.dispose() }) }) describe('descriptions', () => { it('should get descriptions', async () => { let res = await helper.doAction('listDescriptions') expect(res).toBeDefined() expect(res.location).toBeDefined() }) }) describe('switchMatcher()', () => { it('should switch matcher', async () => { await manager.switchMatcher() await manager.start(['--normal', 'location']) manager.session.onInputChange() await manager.session.ui.ready const assertMatcher = (value: string) => { expect(manager.session.listOptions.matcher).toBe(value) } await manager.switchMatcher() assertMatcher('strict') await manager.switchMatcher() assertMatcher('regex') await manager.switchMatcher() assertMatcher('fuzzy') await manager.switchMatcher() assertMatcher('strict') manager.session.listOptions.interactive = true await manager.switchMatcher() assertMatcher('strict') await manager.cancel(true) }) }) describe('loadItems()', () => { it('should ignore cancellation error', async () => { let list: IList = { name: 'cancel', actions: [{ name: 'open', execute: () => {} }], defaultAction: 'open', loadItems: () => Promise.reject(new CancellationError()), } let disposable = manager.registerList(list) await manager.start(['cancel']) disposable.dispose() let line = await helper.getCmdline() expect(line).toBe('') }) it('should load items for list', async () => { let res = await manager.loadItems('location') expect(res.length).toBeGreaterThan(0) Object.assign(manager, { lastSession: undefined }) manager.toggleMode() manager.stop() res = await helper.doAction('listLoadItems', '') expect(res).toBeUndefined() let error = true manager.registerList({ name: 'emitter', actions: [], defaultAction: '', loadItems: () => { let emitter: any = new EventEmitter() let interval let timeout emitter.dispose = () => { emitter.removeAllListeners() clearInterval(interval) clearTimeout(timeout) } if (error) { timeout = setTimeout(() => { emitter.emit('error', new Error('error')) emitter.emit('end') }, 2) } else { timeout = setTimeout(() => { emitter.emit('data', { label: 'foo' }) emitter.emit('end') }, 2) } interval = setInterval(() => { emitter.emit('data', { label: 'bar' }) emitter.emit('error', new Error('error')) }, 10) return emitter } }) await expect(async () => { await manager.loadItems('emitter') }).rejects.toThrow(Error) error = false res = await manager.loadItems('emitter') expect(res.length).toBe(1) await helper.wait(50) }) }) describe('onInsertInput()', () => { it('should handle insert input', async () => { await manager.onInsertInput('k') await manager.onInsertInput('') await manager.start(['--number-select', 'location']) await manager.session.ui.ready await manager.onInsertInput('1') await manager.onInsertInput(String.fromCharCode(129)) let basename = path.basename(__filename) await helper.waitValue(async () => { let bufname = await nvim.call('bufname', ['%']) as string return bufname.includes(basename) }, true) }) it('should ignore invalid input', async () => { await manager.start(['location']) await manager.session.ui.ready await manager.onInsertInput('') await manager.onInsertInput(String.fromCharCode(65533)) await manager.onInsertInput(String.fromCharCode(30)) expect(manager.isActivated).toBe(true) }) it('should ignore insert', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') await helper.listInput('x') expect(manager.isActivated).toBe(true) }) }) describe('parseArgs()', () => { it('should show error for bad option', async () => { manager.parseArgs(['$x', 'location']) await helper.wait(20) let msg = await helper.getCmdline() expect(msg).toMatch('Invalid list option') manager.parseArgs(['-xyz', 'location']) msg = await helper.getCmdline() expect(msg).toMatch('Invalid option') }) it('should parse valid arguments', async () => { let res = manager.parseArgs([]) expect(res.list.name).toBe('lists') res = manager.parseArgs(['lists', '-foo']) expect(res.listArgs).toEqual(['-foo']) }) it('should show error for interactive with list not support interactive', async () => { manager.parseArgs(['--interactive', 'location']) let msg = await helper.getCmdline() expect(msg).toMatch('not supported') }) }) describe('resume()', () => { it('should resume by name', async () => { await events.fire('FocusGained', []) await manager.start(['location']) await manager.session.ui.ready await manager.session.hide() await manager.resume('location') await helper.doAction('listResume') expect(manager.isActivated).toBe(true) await manager.resume('not_exists') let line = await helper.getCmdline() expect(line).toMatch('Can\'t find') }) }) describe('triggerCursorMoved()', () => { it('should triggerCursorMoved autocmd', async () => { let called = 0 let disposable = events.on('CursorMoved', () => { called++ }) Object.assign(events, { _cursor: undefined }) Object.assign(nvim, { isVim: true }) manager.triggerCursorMoved() manager.triggerCursorMoved() Object.assign(nvim, { isVim: false }) await helper.waitValue(() => { return called }, 1) disposable.dispose() }) }) describe('first(), last()', () => { it('should get session by name', async () => { let last: string let list: IList = { name: 'test', actions: [{ name: 'open', execute: item => { last = toArray(item)[0].label } }], defaultAction: 'open', loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'bar' }]) } manager.registerList(list, true) await manager.start(['test']) await manager.session.ui.ready await helper.doAction('listFirst', 'a') await helper.doAction('listLast', 'a') await manager.first('test') expect(last).toBe('foo') await manager.last('test') expect(last).toBe('bar') }) }) describe('registerList()', () => { it('should recreate list', async () => { let fn = jest.fn() let list: IList = { name: 'test', actions: [{ name: 'open', execute: _item => { // noop } }], defaultAction: 'open', loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'bar' }]), dispose: () => { fn() } } manager.registerList(list, true) helper.updateConfiguration('list.source.test.defaultAction', 'open') let disposable = manager.registerList(list, true) disposable.dispose() expect(fn).toHaveBeenCalled() }) }) describe('start()', () => { it('should show error when loadItems throws', async () => { let list: IList = { name: 'test', actions: [{ name: 'open', execute: _item => { } }], defaultAction: 'open', loadItems: () => { throw new Error('test error') } } manager.registerList(list, true) await manager.start(['test']) await helper.wait(20) }) }) describe('list options', () => { it('should respect auto preview option', async () => { await manager.start(['--auto-preview', 'location']) await manager.session.ui.ready await helper.waitFor('winnr', ['$'], 3) let previewWinnr = await nvim.call('coc#list#has_preview') expect(previewWinnr).toBe(2) let bufnr = await nvim.call('winbufnr', previewWinnr) as number let buf = nvim.createBuffer(bufnr) let name = await buf.name expect(name).toMatch('manager.test.ts') await nvim.eval('feedkeys("j", "in")') await helper.wait(30) let winnr = await nvim.call('coc#list#has_preview') expect(winnr).toBe(previewWinnr) }) it('should respect input option', async () => { await manager.start(['--input=foo', 'location']) await manager.session.ui.ready let line = await helper.getCmdline() expect(line).toMatch('foo') expect(manager.isActivated).toBe(true) }) it('should respect regex filter', async () => { await manager.start(['--input=f.o', '--regex', 'location']) await manager.session.ui.ready let item = await manager.session?.ui.item expect(item.label).toMatch('foo') await manager.session.hide() await manager.start(['--input=f.o', '--ignore-case', '--regex', 'location']) await manager.session.ui.ready item = await manager.session?.ui.item expect(item.label).toMatch('foo') }) it('should respect normal option', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready let line = await helper.getCmdline() expect(line).toBe('') }) it('should respect nosort option', async () => { await manager.start(['--ignore-case', '--no-sort', 'location']) await manager.session.ui.ready await nvim.input('oo') await helper.waitValue(async () => { let line = await nvim.call('getline', ['.']) as string return line.includes('foo') }, true) }) it('should respect ignorecase option', async () => { await manager.start(['--ignore-case', '--strict', 'location']) await manager.session.ui.ready expect(manager.isActivated).toBe(true) await nvim.input('bar') await helper.waitValue(() => { return manager.session?.ui.length }, 1) let line = await nvim.line expect(line).toMatch('Bar') }) it('should respect top & height option', async () => { await manager.start(['--top', '--height=2', 'location']) await manager.session.ui.ready let nr = await nvim.call('winnr') expect(nr).toBe(1) let win = await nvim.window let height = await win.height expect(height).toBe(2) }) it('should respect number select option', async () => { await manager.start(['--number-select', 'location']) await manager.session.ui.ready await nvim.eval('feedkeys("2", "in")') let lnum = locations[1].lnum await helper.waitFor('line', ['.'], lnum) }) it('should respect tab option', async () => { await manager.start(['--tab', '--auto-preview', 'location']) await manager.session.ui.ready await helper.waitFor('tabpagenr', ['$'], 2) }) }) }) ================================================ FILE: src/__tests__/list/mappings.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import path from 'path' import { CancellationToken, Disposable } from 'vscode-languageserver-protocol' import BasicList from '../../list/basic' import listConfiguration, { ListConfiguration } from '../../list/configuration' import manager from '../../list/manager' import { IList, ListContext, ListItem } from '../../list/types' import { QuickfixItem } from '../../types' import { disposeAll } from '../../util/index' import window from '../../window' import helper from '../helper' class TestList extends BasicList { public name = 'test' public timeout = 3000 public text = 'test' public detail = 'detail' public loadItems(_context: ListContext, token: CancellationToken): Promise { return new Promise(resolve => { let timer = setTimeout(() => { resolve([{ label: this.text }]) }, this.timeout) token.onCancellationRequested(() => { if (timer) { clearTimeout(timer) resolve([]) } }) }) } } let nvim: Neovim let disposables: Disposable[] = [] const locations: ReadonlyArray = [{ filename: __filename, col: 2, lnum: 1, text: 'foo' }, { filename: __filename, col: 1, lnum: 2, text: 'Bar' }, { filename: __filename, col: 1, lnum: 3, text: 'option' }] async function waitPreviewWindow(): Promise { for (let i = 0; i < 40; i++) { await helper.wait(50) let has = await nvim.call('coc#list#has_preview') as number if (has > 0) return } throw new Error('timeout after 2s') } const lineList: IList = { name: 'lines', actions: [{ name: 'open', execute: async item => { await window.moveTo({ line: (item as ListItem).data.line, character: 0 }) // noop } }], defaultAction: 'open', async loadItems(_context, _token): Promise { let lines = [] for (let i = 0; i < 100; i++) { lines.push(i.toString()) } return lines.map((line, idx) => ({ label: line, data: { line: idx } })) } } beforeAll(async () => { await helper.setup() nvim = helper.nvim await nvim.setVar('coc_jump_locations', locations) }) afterAll(async () => { disposeAll(disposables) await helper.shutdown() }) afterEach(async () => { manager.reset() await helper.reset() }) describe('isValidAction()', () => { it('should check invalid action', () => { let mappings = manager.mappings expect(mappings.isValidAction('foo')).toBe(false) expect(mappings.isValidAction('do:switch')).toBe(true) expect(mappings.isValidAction('eval:@*')).toBe(true) expect(mappings.isValidAction('undefined:undefined')).toBe(false) }) }) describe('User mappings', () => { it('should not throw when session not exists', async () => { let mappings = manager.mappings let res = await mappings.navigate(true) expect(res).toBe(false) res = await mappings.navigate(false) expect(res).toBe(false) }) it('should show warning for invalid key', async () => { expect(ListConfiguration).toBeDefined() expect(listConfiguration.fixKey('')).toBe('') listConfiguration.fixKey('': 'action:tabe', }) await helper.wait(30) msg = await helper.getCmdline() revert() expect(msg).toMatch('Invalid configuration') revert = helper.updateConfiguration('list.insertMappings', { '': 'foo:bar', }) await helper.wait(30) msg = await helper.getCmdline() revert() expect(msg).toMatch('Invalid configuration') }) it('should execute action keymap', async () => { let revert = helper.updateConfiguration('list.insertMappings', { '': 'action:quickfix', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let buftype = await nvim.eval('&buftype') expect(buftype).toBe('quickfix') revert() }) it('should execute expr keymap', async () => { await helper.mockFunction('TabOpen', 'quickfix') helper.updateConfiguration('list.insertMappings', { '': 'expr:TabOpen', }) helper.updateConfiguration('list.normalMappings', { t: 'expr:TabOpen', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let buftype = await nvim.eval('&buftype') expect(buftype).toBe('quickfix') await nvim.command('close') await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('t') buftype = await nvim.eval('&buftype') expect(buftype).toBe('quickfix') }) it('should execute do mappings', async () => { helper.updateConfiguration('list.previousKeymap', '') helper.updateConfiguration('list.nextKeymap', '') helper.updateConfiguration('list.insertMappings', { '': 'do:next', '': 'do:previous', '': 'do:exit', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let item = await manager.session?.ui.item expect(item.label).toMatch(locations[1].text) await helper.listInput('') item = await manager.session?.ui.item expect(item.label).toMatch(locations[0].text) await helper.listInput('') item = await manager.session?.ui.item expect(item.label).toMatch(locations[1].text) await helper.listInput('') item = await manager.session?.ui.item expect(item.label).toMatch(locations[0].text) await helper.listInput('') expect(manager.isActivated).toBe(false) }) it('should execute prompt mappings', async () => { helper.updateConfiguration('list.insertMappings', { '': 'prompt:previous', '': 'prompt:next', '': 'prompt:start', '': 'prompt:end', '': 'prompt:left', '': 'prompt:right', '': 'prompt:deleteforward', '': 'prompt:deletebackward', '': 'prompt:removetail', '': 'prompt:removeahead', }) await manager.start(['location']) await manager.session.ui.ready for (let key of ['', '', '', '', '', '', '', '', '', '']) { await helper.listInput(key) } expect(manager.isActivated).toBe(true) }) it('should execute feedkeys keymap', async () => { helper.updateConfiguration('list.insertMappings', { '': 'feedkeys:\\', '': 'feedkeys!:\\', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') await helper.waitFor('line', ['.'], locations.length) await helper.listInput('') }) it('should execute normal keymap', async () => { helper.updateConfiguration('list.insertMappings', { '': 'normal:G', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let line = await nvim.call('line', '.') expect(line).toBe(locations.length) }) it('should execute command keymap', async () => { helper.updateConfiguration('list.insertMappings', { '': 'command:wincmd p', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') expect(manager.isActivated).toBe(true) let winnr = await nvim.call('winnr') expect(winnr).toBe(1) }) it('should execute call keymap', async () => { await helper.mockFunction('Test', 1) helper.updateConfiguration('list.insertMappings', { '': 'call:Test', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') expect(manager.isActivated).toBe(true) }) it('should insert clipboard register to prompt', async () => { helper.updateConfiguration('list.insertMappings', { '': 'prompt:paste', }) await nvim.command('let @* = "foobar"') await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let { input } = manager.prompt expect(input).toMatch('foobar') await nvim.command('let @* = ""') await helper.listInput('') expect(manager.prompt.input).toMatch('foobar') }) it('should insert text from default register to prompt', async () => { helper.updateConfiguration('list.insertMappings', { '': 'eval:@@', }) await nvim.command('let @@ = "bar"') await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let { input } = manager.prompt expect(input).toMatch('bar') }) }) describe('doAction()', () => { it('should throw when action not found', async () => { let mappings = manager.mappings let fn = async () => { await mappings.doAction('foo:bar') } await expect(fn()).rejects.toThrow(/doesn't exist/) }) it('should not throw when session does not exist', async () => { let mappings = manager.mappings await mappings.doAction('do:selectall') await mappings.doAction('do:help') await mappings.doAction('do:refresh') await mappings.doAction('do:toggle') await mappings.doAction('do:jumpback') await mappings.doAction('prompt:previous') await mappings.doAction('prompt:next') await mappings.doAction('do:refresh') }) it('should not throw when action name does not exist', async () => { await helper.mockFunction('MyExpr', '') let mappings = manager.mappings await mappings.doAction('expr', 'MyExpr') }) }) describe('getAction()', () => { it('should throw for invalid action', async () => { let mappings = manager.mappings let fn = () => { mappings.getAction('foo') } expect(fn).toThrow(Error) fn = () => { mappings.getAction('do:bar') } expect(fn).toThrow(Error) }) }) describe('Default normal mappings', () => { it('should invoke action', async () => { await manager.start(['--normal', '--no-quit', 'location']) await manager.session.ui.ready let winid = manager.session.ui.winid await helper.listInput('t') let nr = await nvim.call('tabpagenr') expect(nr).toBe(2) await nvim.call('win_gotoid', [winid]) await helper.listInput('s') let winnr = await nvim.call('winnr', ['$']) expect(winnr).toBe(3) await nvim.call('win_gotoid', [winid]) await helper.listInput('d') let filename = await nvim.call('expand', ['%']) expect(filename).toMatch(path.basename(__filename)) await nvim.call('win_gotoid', [winid]) await helper.listInput('') filename = await nvim.call('expand', ['%']) expect(filename).toMatch(path.basename(__filename)) }) it('should select all items by ', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('') let selected = manager.session?.ui.selectedItems expect(selected.length).toBe(locations.length) }) it('should stop by ', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('') let loading = manager.session?.worker.isLoading expect(loading).toBe(false) }) it('should jump back by ', async () => { let doc = await helper.createDocument() await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('') let bufnr = await nvim.call('bufnr', ['%']) expect(bufnr).toBe(doc.bufnr) }) it('should scroll preview window by , ', async () => { await helper.createDocument() await manager.start(['--auto-preview', '--normal', 'location']) await manager.session.ui.ready await waitPreviewWindow() let winnr = await nvim.call('coc#list#has_preview') as number let winid = await nvim.call('win_getid', [winnr]) await helper.listInput('') let res = await nvim.call('getwininfo', [winid]) expect(res[0].topline).toBeGreaterThan(1) await helper.listInput('') res = await nvim.call('getwininfo', [winid]) expect(res[0].topline).toBeLessThan(7) }) it('should insert command by :', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput(':') await nvim.eval('feedkeys("let g:x = 1\\", "in")') await helper.waitValue(() => { return nvim.getVar('x') }, 1) }) it('should select action by ', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready let p = helper.listInput('') await helper.wait(50) await nvim.input('t') await p let nr = await nvim.call('tabpagenr') expect(nr).toBe(2) }) it('should preview by p', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('p') let winnr = await nvim.call('coc#list#has_preview') expect(winnr).toBe(2) }) it('should stop task by ', async () => { disposables.push(manager.registerList(new TestList())) let p = manager.start(['--normal', 'test']) await helper.wait(50) await nvim.input('') await p let len = manager.session?.ui.length expect(len).toBe(0) }) it('should cancel list by ', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await nvim.eval('feedkeys("\\", "in")') await helper.waitValue(() => { return manager.isActivated }, false) }) it('should reload list by ', async () => { let list = new TestList() list.timeout = 0 disposables.push(manager.registerList(list)) await manager.start(['--normal', 'test']) await manager.session.ui.ready list.text = 'new' await helper.listInput('') let line = await nvim.line expect(line).toMatch('new') }) it('should toggle selection ', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput(' ') await helper.waitValue(() => { return manager.session?.ui.selectedItems.length }, 1) await helper.listInput('k') await helper.listInput(' ') await helper.waitValue(() => { return manager.session?.ui.selectedItems.length }, 0) }) it('should change to insert mode by i, o, a', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready let keys = ['i', 'I', 'o', 'O', 'a', 'A'] for (let key of keys) { await helper.listInput(key) let mode = manager.prompt.mode expect(mode).toBe('insert') await helper.listInput('') mode = manager.prompt.mode expect(mode).toBe('normal') } }) it('should show help by ?', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('?') let bufname = await nvim.call('bufname', '%') expect(bufname).toBe('[LIST HELP]') }) }) describe('list insert mappings', () => { it('should open by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let bufname = await nvim.call('expand', ['%:p']) expect(bufname).toMatch('mappings.test.ts') }) it('should paste input by ', async () => { await nvim.command('let @* = "foo"') await nvim.command('let @@ = "foo"') await nvim.call('setreg', ['*', 'foo']) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let input = manager.prompt.input expect(input).toBe('foo') }) it('should insert register content by ', async () => { await nvim.command('let @* = "foo"') await nvim.command('let @@ = "foo"') await nvim.call('setreg', ['*', 'foo']) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') await helper.listInput('*') let input = manager.prompt.input expect(input).toBe('foo') await helper.listInput('') await helper.listInput('<') input = manager.prompt.input expect(input).toBe('foo') manager.prompt.reset() }) it('should cancel by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') expect(manager.isActivated).toBe(false) }) it('should select action by insert ', async () => { await manager.start(['location']) await manager.session.ui.ready let p = helper.listInput('') await helper.wait(50) await nvim.input('d') await p let bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch(path.basename(__filename)) }) it('should select action for visual selected items', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.waitPrompt() await nvim.input('V') await helper.wait(30) await nvim.input('2') await helper.wait(30) await nvim.input('j') await helper.wait(30) await manager.doAction('quickfix') let buftype = await nvim.eval('&buftype') expect(buftype).toBe('quickfix') }) it('should stop loading by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') expect(manager.isActivated).toBe(true) }) it('should reload by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') expect(manager.isActivated).toBe(true) }) it('should change to normal mode by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') expect(manager.isActivated).toBe(true) }) it('should select line by and ', async () => { await manager.start(['location']) await manager.session.ui.ready await nvim.eval('feedkeys("\\", "in")') await nvim.eval('feedkeys("\\", "in")') expect(manager.isActivated).toBe(true) let line = await nvim.line expect(line).toMatch('foo') }) it('should move cursor by and ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('f') await helper.listInput('') await helper.listInput('') await helper.listInput('a') await helper.listInput('') await helper.listInput('') await helper.listInput('c') let input = manager.prompt.input let mode = manager.prompt.mode manager.prompt.input = input manager.prompt.mode = mode await helper.listInput('') manager.prompt.removeNext() manager.prompt.removeNext() manager.prompt.removeNext() manager.prompt.removeNext() expect(input).toBe('afc') }) it('should move cursor by leftword and rightword', async () => { let revert = helper.updateConfiguration('list.insertMappings', { '': 'prompt:leftword', '': 'prompt:rightword', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('aaa bbb ccc') // -> aaa bbb ccc| await helper.listInput('') // -> aaa bbb |ccc await helper.listInput('') // -> aaa |bbb ccc await helper.listInput('ddd ') // -> aaa ddd |bbb ccc await helper.listInput('') // -> aaa ddd bbb |ccc await helper.listInput('eee ') // -> aaa ddd bbb eee |ccc expect(manager.mappings.hasUserMapping('insert', '')).toBe(true) expect(manager.mappings.hasUserMapping('insert', '')).toBe(true) let input = manager.prompt.input revert() expect(input).toBe('aaa ddd bbb eee ccc') }) it('should move cursor by and ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('ff') await helper.listInput('') await helper.listInput('') await helper.listInput('') let input = manager.prompt.input manager.prompt.removeWord() manager.prompt.removeWord() manager.prompt.removeTail() manager.prompt.removeTail() expect(input).toBe('ff') }) it('should move cursor by ', async () => { disposables.push(manager.registerList(lineList)) await manager.start(['lines']) await manager.session.ui.ready await helper.listInput('') await helper.listInput('') await helper.listInput('') }) it('should scroll window by and ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') await helper.listInput('') }) it('should change input by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('f') await helper.listInput('') let input = manager.prompt.input expect(input).toBe('') }) it('should change input by ', async () => { let revert = helper.updateConfiguration('list.insertMappings', { '': 'prompt:removetail', }) await manager.start(['location']) await manager.session.ui.ready await helper.listInput('f') await helper.listInput('o') await helper.listInput('o') await helper.listInput('') await helper.listInput('') expect(manager.mappings.hasUserMapping('insert', '')).toBe(true) let input = manager.prompt.input revert() expect(input).toBe('') }) it('should change input by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('f') await helper.listInput('') let input = manager.prompt.input expect(input).toBe('') }) it('should change input by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('f') await helper.listInput('a') await helper.listInput('') let input = manager.prompt.input expect(input).toBe('') }) it('should change input by ', async () => { await manager.start(['--input=a', 'location']) await manager.session.ui.ready await helper.listInput('') let input = manager.prompt.input expect(input).toBe('') }) it('should change input by and ', async () => { async function session(input: string): Promise { await manager.start(['location']) await manager.session.ui.ready for (let ch of input) { await helper.listInput(ch) } await manager.cancel() } await session('foo') await session('bar') await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let input = manager.prompt.input expect(input.length).toBeGreaterThan(0) await helper.listInput('') input = manager.prompt.input expect(input.length).toBeGreaterThan(0) }) it('should change matcher by ', async () => { await manager.start(['location']) await manager.session.ui.ready await helper.listInput('') let matcher = manager.session?.listOptions.matcher expect(matcher).toBe('strict') await helper.listInput('') matcher = manager.session?.listOptions.matcher expect(matcher).toBe('regex') await helper.listInput('f') let len = manager.session?.ui.length expect(len).toBeGreaterThan(0) }) }) describe('evalExpression', () => { it('should exit list', async () => { helper.updateConfiguration('list.normalMappings', { t: 'do:exit', }) await manager.start(['--normal', 'location']) await manager.session.ui.ready expect(manager.mappings.hasUserMapping('normal', 't')).toBe(true) await helper.listInput('t') expect(manager.isActivated).toBe(false) }) it('should cancel prompt', async () => { helper.updateConfiguration('list.normalMappings', { t: 'do:cancel', }) await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('t') let res = await nvim.call('coc#prompt#activated') expect(res).toBe(0) }) it('should invoke normal command', async () => { let revert = helper.updateConfiguration('list.normalMappings', { x: 'normal!:G' }) await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput('x') revert() let lnum = await nvim.call('line', ['.']) expect(lnum).toBeGreaterThan(1) }) it('should toggle, scroll preview', async () => { let revert = helper.updateConfiguration('list.normalMappings', { '': 'do:toggle', a: 'do:toggle', b: 'do:previewtoggle', c: 'do:previewup', d: 'do:previewdown', e: 'prompt:insertregister', f: 'do:stop', g: 'do:togglemode', }) await manager.start(['--normal', 'location']) await manager.session.ui.ready await helper.listInput(' ') for (let key of ['a', 'b', 'c', 'd', 'e', 'f', 'g']) { await helper.listInput(key) } revert() expect(manager.isActivated).toBe(true) }) }) ================================================ FILE: src/__tests__/list/session.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import BasicList from '../../list/basic' import manager from '../../list/manager' import Prompt from '../../list/prompt' import ListSession from '../../list/session' import { IList, ListItem } from '../../list/types' import { disposeAll } from '../../util' import helper from '../helper' let labels: string[] = [] let lastItem: string let lastItems: ListItem[] class SimpleList extends BasicList { public name = 'simple' public detail = 'detail' public options = [{ name: 'foo', description: 'foo' }] constructor() { super() this.addAction('open', item => { lastItem = item.label }, { tabPersist: true }) this.addMultipleAction('multiple', items => { lastItems = items }) this.addAction('parallel', async () => { await helper.wait(100) }, { parallel: true }) this.addAction('reload', item => { lastItem = item.label }, { persist: true, reload: true }) } public loadItems(): Promise { return Promise.resolve(labels.map(s => { return { label: s } as ListItem })) } } let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) manager.reset() await helper.reset() }) describe('list session', () => { describe('doDefaultAction()', () => { it('should throw error when default action does not exist', async () => { labels = ['a', 'b', 'c'] let list = new SimpleList() list.defaultAction = 'foo' let len = list.actions.length list.actions.splice(0, len) disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready let err try { await manager.session.first() } catch (e) { err = e } expect(err).toBeDefined() err = null try { await manager.session.last() } catch (e) { err = e } expect(err).toBeDefined() }) }) describe('doItemAction()', () => { it('should invoke multiple action', async () => { labels = ['a', 'b', 'c'] let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready await ui.selectAll() await manager.doAction('multiple') expect(lastItems.length).toBe(3) lastItems = undefined await manager.session.doPreview(0) await manager.doAction('not_exists') let line = await helper.getCmdline() expect(line).toMatch('not found') }) it('should invoke parallel action', async () => { labels = ['a', 'b', 'c'] let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready await ui.selectAll() let d = Date.now() await manager.doAction('parallel') expect(Date.now() - d).toBeLessThan(300) }) it('should support tabPersist action', async () => { labels = ['a', 'b', 'c'] let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', '--tab', 'simple']) let ui = manager.session.ui await ui.ready await manager.doAction('open') let tabnr = await nvim.call('tabpagenr') expect(tabnr).toBeGreaterThan(1) let win = nvim.createWindow(ui.winid) let valid = await win.valid expect(valid).toBe(true) }) it('should invoke reload action', async () => { labels = ['a', 'b', 'c'] let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready labels = ['d', 'e'] await manager.doAction('reload') await helper.wait(50) let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual(['d', 'e']) }) }) describe('reloadItems()', () => { it('should not reload items when window is hidden', async () => { let fn = jest.fn() let list: IList = { name: 'reload', defaultAction: 'open', actions: [{ name: 'open', execute: () => {} }], loadItems: () => { fn() return Promise.resolve([]) } } disposables.push(manager.registerList(list)) await manager.start(['--normal', 'reload']) let ui = manager.session.ui await ui.ready await manager.cancel(true) let ses = manager.getSession('reload') await ses.reloadItems() expect(fn).toHaveBeenCalledTimes(1) }) }) describe('resume()', () => { it('should do preview on resume', async () => { labels = ['a', 'b', 'c'] let lastItem let list = new SimpleList() list.actions.push({ name: 'preview', execute: item => { lastItem = item } }) disposables.push(manager.registerList(list)) await manager.start(['--normal', '--auto-preview', 'simple']) let ui = manager.session.ui await ui.ready await ui.selectLines(1, 2) await helper.wait(50) await nvim.call('coc#window#close', [ui.winid]) await helper.wait(100) await manager.session.resume() await helper.wait(100) expect(lastItem).toBeDefined() }) }) describe('jumpBack()', () => { it('should jump back', async () => { let win = await nvim.window labels = ['a', 'b', 'c'] let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready manager.session.jumpBack() await helper.wait(50) let winid = await nvim.call('win_getid') expect(winid).toBe(win.id) }) }) describe('hide()', () => { it('should not throw when window undefined', async () => { let session = new ListSession(nvim, new Prompt(nvim), new SimpleList(), { reverse: true, numberSelect: true, autoPreview: true, first: false, input: 'test', interactive: false, matcher: 'strict', ignorecase: true, position: 'top', mode: 'normal', noQuit: false, sort: false }, []) await expect(async () => { await session.call('fn_not_exists') }).rejects.toThrow(Error) await session.doPreview(0) await session.first() await session.hide(false, true) let worker: any = session.worker worker._onDidChangeItems.fire({ items: [] }) worker._onDidChangeLoading.fire(false) }) }) describe('doNumberSelect()', () => { async function create(len: number): Promise { labels = [] for (let i = 0; i < len; i++) { let code = 'a'.charCodeAt(0) + i labels.push(String.fromCharCode(code)) } let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', '--number-select', 'simple']) let ui = manager.session.ui await ui.ready return manager.session } it('should return false for invalid number', async () => { let session = await create(5) let res = await session.doNumberSelect('a') expect(res).toBe(false) res = await session.doNumberSelect('8') expect(res).toBe(false) }) it('should consider 0 as 10', async () => { let session = await create(15) let res = await session.doNumberSelect('0') expect(res).toBe(true) expect(lastItem).toBe('j') }) }) }) describe('showHelp()', () => { it('should show description and options in help', async () => { labels = ['a', 'b', 'c'] let list = new SimpleList() disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready await manager.session.showHelp() let lines = await nvim.call('getline', [1, '$']) as string[] expect(lines.indexOf('DESCRIPTION')).toBeGreaterThan(0) expect(lines.indexOf('ARGUMENTS')).toBeGreaterThan(0) }) }) describe('chooseAction()', () => { it('should filter actions not have shortcuts', async () => { labels = ['a', 'b', 'c'] let fn = jest.fn() let list = new SimpleList() list.actions.push({ name: 'a', execute: () => { fn() } }) list.actions.push({ name: 'b', execute: () => { } }) list.actions.push({ name: 'ab', execute: () => { } }) disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) await manager.session.ui.ready let p = manager.session.chooseAction() await helper.wait(50) await nvim.input('a') await p expect(fn).toHaveBeenCalled() }) it('should choose action by menu picker', async () => { helper.updateConfiguration('list.menuAction', true) labels = ['a', 'b', 'c'] let fn = jest.fn() let list = new SimpleList() let len = list.actions.length list.actions.splice(0, len) list.actions.push({ name: 'a', execute: () => { fn() } }) list.actions.push({ name: 'b', execute: () => { fn() } }) disposables.push(manager.registerList(list)) await manager.start(['--normal', 'simple']) await manager.session.ui.ready let p = manager.session.chooseAction() await helper.waitPrompt() await nvim.input('') await p }) }) ================================================ FILE: src/__tests__/list/source-funcs.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationToken } from 'vscode-languageserver-protocol' import { DocumentSymbol, Location, Range, SymbolKind } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import which from 'which' import BasicList, { toVimFiletype } from '../../list/basic' import { fixWidth, formatListItems, formatPath, formatUri, UnformattedListItem } from '../../list/formatting' import { getExtensionPrefix, getExtensionPriority, sortExtensionItem } from '../../list/source/extensions' import { mruScore } from '../../list/source/lists' import { contentToItems, getFilterText, loadCtagsSymbols, symbolsToListItems } from '../../list/source/outline' import { sortSymbolItems, toTargetLocation } from '../../list/source/symbols' import { ListItem } from '../../list/types' import { os, path } from '../../util/node' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) class SimpleList extends BasicList { public name = 'simple' public defaultAction: 'preview' constructor() { super() } public loadItems(): Promise { return Promise.resolve([]) } } describe('List util', () => { it('should get list score', () => { expect(mruScore(['foo'], 'foo')).toBe(1) expect(mruScore([], 'foo')).toBe(-1) }) }) describe('BasicList util', () => { let list: SimpleList beforeAll(() => { list = new SimpleList() }) it('should get filetype', async () => { expect(toVimFiletype('latex')).toBe('tex') expect(toVimFiletype('foo')).toBe('foo') }) it('should convert uri', async () => { let uri = URI.file(__filename).toString() let res = await list.convertLocation(uri) expect(res.uri).toBe(uri) }) it('should convert location with line', async () => { let uri = URI.file(__filename).toString() let res = await list.convertLocation({ uri, line: 'convertLocation()', text: 'convertLocation' }) expect(res.uri).toBe(uri) res = await list.convertLocation({ uri, line: 'convertLocation()' }) expect(res.uri).toBe(uri) }) it('should convert location with custom schema', async () => { let uri = 'test:///foo' let res = await list.convertLocation({ uri, line: 'convertLocation()' }) expect(res.uri).toBe(uri) }) }) describe('Outline util', () => { it('should getFilterText', () => { expect(getFilterText(DocumentSymbol.create('name', '', SymbolKind.Function, Range.create(0, 0, 0, 1), Range.create(0, 0, 0, 1)), 'kind')).toBe('name') expect(getFilterText(DocumentSymbol.create('name', '', SymbolKind.Function, Range.create(0, 0, 0, 1), Range.create(0, 0, 0, 1)), '')).toBe('nameFunction') }) it('should load items by ctags', async () => { let doc = await workspace.document let spy = jest.spyOn(which, 'sync').mockImplementation(() => { return '' }) let items = await loadCtagsSymbols(doc, nvim, CancellationToken.None) expect(items).toEqual([]) spy.mockRestore() doc = await helper.createDocument(__filename) items = await loadCtagsSymbols(doc, nvim, CancellationToken.None) expect(Array.isArray(items)).toBe(true) }) it('should convert symbols to list items', async () => { let symbols: DocumentSymbol[] = [] symbols.push(DocumentSymbol.create('function', '', SymbolKind.Function, Range.create(1, 0, 1, 1), Range.create(1, 0, 1, 1))) symbols.push(DocumentSymbol.create('class', '', SymbolKind.Class, Range.create(0, 0, 0, 1), Range.create(0, 0, 0, 1))) let items = symbolsToListItems(symbols, 'lsp:/1', 'class') expect(items.length).toBe(1) expect(items[0].data.kind).toBe('Class') }) it('should convert to list items', async () => { let doc = await workspace.document expect(contentToItems('a\tb\t2\td\n\n', doc).length).toBe(1) }) }) describe('Extensions util', () => { it('should sortExtensionItem', () => { expect(sortExtensionItem({ data: { priority: 1 } }, { data: { priority: 0 } })).toBe(-1) expect(sortExtensionItem({ data: { id: 'a' } }, { data: { id: 'b' } })).toBe(1) expect(sortExtensionItem({ data: { id: 'b' } }, { data: { id: 'a' } })).toBe(-1) }) it('should get extension prefix', () => { expect(getExtensionPrefix('')).toBe('+') expect(getExtensionPrefix('disabled')).toBe('-') expect(getExtensionPrefix('activated')).toBe('*') expect(getExtensionPrefix('unknown')).toBe('?') }) it('should get extension priority', () => { expect(getExtensionPriority('')).toBe(0) expect(getExtensionPriority('unknown')).toBe(2) expect(getExtensionPriority('activated')).toBe(1) expect(getExtensionPriority('disabled')).toBe(-1) }) }) describe('Symbols util', () => { it('should convert to location', () => { let res = toTargetLocation({ uri: 'untitled:1' }) expect(Location.is(res)).toBe(true) }) }) describe('formatting', () => { it('should format path', () => { let base = path.basename(__filename) expect(formatPath('short', 'home')).toMatch('home') expect(formatPath('hidden', 'path')).toBe('') expect(formatPath('full', __filename)).toMatch(base) expect(formatPath('short', __filename)).toMatch(base) expect(formatPath('filename', __filename)).toMatch(base) }) it('should format uri', () => { let cwd = process.cwd() expect(formatUri('http://www.example.com', cwd)).toMatch('http') expect(formatUri(URI.file(__filename).toString(), cwd)).toMatch('source') expect(formatUri(URI.file(os.tmpdir()).toString(), cwd)).toMatch(os.tmpdir()) }) it('should fixWidth', () => { expect(fixWidth('a'.repeat(10), 2)).toBe('a.') }) it('should sort symbols', () => { const assert = (a, b, n) => { expect(sortSymbolItems(a, b)).toBe(n) } assert({ data: { score: 1 } }, { data: { score: 2 } }, 1) assert({ data: { kind: 1 } }, { data: { kind: 2 } }, -1) assert({ data: { file: 'aa' } }, { data: { file: 'b' } }, 1) }) it('should format list items', () => { expect(formatListItems(false, [])).toEqual([]) let items: UnformattedListItem[] = [{ label: ['a', 'b', 'c'] }] expect(formatListItems(false, items)).toEqual([{ label: 'a\tb\tc' }]) items = [{ label: ['a', 'b', 'c'] }, { label: ['foo', 'bar', 'go'] }] expect(formatListItems(true, items)).toEqual([{ label: 'a \tb \tc ' }, { label: 'foo\tbar\tgo' }]) // items with different column counts (e.g. local vs non-local extensions) items = [{ label: ['* foo', '[RTP]', '1.0.0', '/tmp/foo'] }, { label: ['+ bar', '2.0.0', '/tmp/bar'] }] let result = formatListItems(true, items) expect(result[0].label.split('\t')).toEqual(['* foo', '[RTP]', '1.0.0 ', '/tmp/foo']) expect(result[1].label.split('\t')).toEqual(['+ bar', '2.0.0', '/tmp/bar']) }) }) ================================================ FILE: src/__tests__/list/sources.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import { v4 as uuid } from 'uuid' import { CancellationToken, Diagnostic, DiagnosticSeverity, Disposable, DocumentLink, Emitter, Location, Position, Range, SymbolInformation, SymbolKind, SymbolTag, TextEdit } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import diagnosticManager, { DiagnosticItem } from '../../diagnostic/manager' import events from '../../events' import extensions from '../../extension/index' import { ExtensionInfo, ExtensionManager } from '../../extension/manager' import { ExtensionStat } from '../../extension/stat' import languages from '../../languages' import BasicList, { PreviewOptions } from '../../list/basic' import manager from '../../list/manager' import DiagnosticsList, { convertToLabel } from '../../list/source/diagnostics' import ExtensionList from '../../list/source/extensions' import FolderList from '../../list/source/folders' import OutlineList from '../../list/source/outline' import SymbolsList from '../../list/source/symbols' import { ListArgument, ListContext, ListItem, ListOptions } from '../../list/types' import Document from '../../model/document' import services, { IServiceProvider, ServiceStat } from '../../services' import { QuickfixItem } from '../../types' import { disposeAll } from '../../util' import * as extension from '../../util/extensionRegistry' import { path } from '../../util/node' import { Registry } from '../../util/registry' import window from '../../window' import workspace from '../../workspace' import Parser from '../handler/parser' import helper from '../helper' let listItems: ListItem[] = [] class OptionList extends BasicList { public name = 'option' public options: ListArgument[] = [{ name: '-w, -word', description: 'word' }, { name: '-i, -input INPUT', hasValue: true, description: 'input' }, { key: 'name', description: '', name: '-name' }] constructor() { super() this.addLocationActions() } public loadItems(_context: ListContext, _token: CancellationToken): Promise { return Promise.resolve(listItems) } } let previewOptions: PreviewOptions class SimpleList extends BasicList { public name = 'simple' public defaultAction: 'preview' constructor() { super() this.addAction('preview', async (_item, context) => { await this.preview(previewOptions, context) }) } public loadItems(): Promise { return Promise.resolve(['a', 'b', 'c'].map((s, idx) => { return { label: s, location: Location.create('test:///a', Range.create(idx, 0, idx + 1, 0)) } as ListItem })) } } async function createContext(option: Partial): Promise { let buffer = await nvim.buffer let window = await nvim.window return { args: [], buffer, cwd: process.cwd(), input: '', listWindow: nvim.createWindow(1002), options: Object.assign({ position: 'bottom', reverse: false, input: '', ignorecase: false, smartcase: false, interactive: false, sort: false, mode: 'normal', matcher: 'strict', autoPreview: false, numberSelect: false, noQuit: false, first: false }, option), window } } let disposables: Disposable[] = [] let nvim: Neovim const locations: QuickfixItem[] = [{ filename: __filename, range: Range.create(0, 0, 0, 6), targetRange: Range.create(0, 0, 0, 6), text: 'foo', type: 'Error' }, { filename: __filename, range: Range.create(2, 0, 2, 6), text: 'Bar', type: 'Warning' }, { filename: __filename, range: Range.create(3, 0, 4, 6), text: 'multiple' }, { filename: path.join(os.tmpdir(), '3195369f-5b9f-4c46-99cd-6007c0224595'), range: Range.create(3, 0, 4, 6), text: 'tmpdir' }] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { manager.dispose() await helper.shutdown() }) afterEach(async () => { listItems = [] disposeAll(disposables) manager.reset() await helper.reset() }) describe('configuration', () => { beforeEach(() => { let list = new OptionList() manager.registerList(list) }) it('should change default options', async () => { helper.updateConfiguration('list.source.option.defaultOptions', ['--normal']) await manager.start(['option']) await manager.session.ui.ready const mode = manager.prompt.mode expect(mode).toBe('normal') }) it('should change default action', async () => { helper.updateConfiguration('list.source.option.defaultAction', 'split') await manager.start(['option']) await manager.session.ui.ready const action = manager.session.defaultAction expect(action.name).toBe('split') await manager.session.doAction() let tab = await nvim.tabpage let wins = await tab.windows expect(wins.length).toBeGreaterThan(1) }) it('should change default arguments', async () => { helper.updateConfiguration('list.source.option.defaultArgs', ['-word']) await manager.start(['option']) await manager.session.ui.ready const context = manager.session.context expect(context.args).toEqual(['-word']) }) }) describe('BasicList', () => { describe('parse arguments', () => { it('should parse args #1', () => { let list = new OptionList() let res = list.parseArguments(['-w']) expect(res).toEqual({ word: true }) }) it('should parse args #2', () => { let list = new OptionList() let res = list.parseArguments(['-word']) expect(res).toEqual({ word: true }) }) it('should parse args #3', () => { let list = new OptionList() let res = list.parseArguments(['-input', 'foo']) expect(res).toEqual({ input: 'foo' }) }) }) describe('jumpTo()', () => { let list: OptionList beforeAll(() => { list = new OptionList() }) it('should jump to uri', async () => { let uri = URI.file(__filename).toString() let ctx = await createContext({ position: 'tab' }) await list.jumpTo(uri, null, ctx) let bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch('sources.test.ts') }) it('should jump to location', async () => { let uri = URI.file(__filename).toString() let loc = Location.create(uri, Range.create(0, 0, 1, 0)) await list.jumpTo(loc, 'edit') let bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch('sources.test.ts') }) it('should jump to location with empty range', async () => { let uri = URI.file(__filename).toString() let loc = Location.create(uri, Range.create(0, 0, 0, 0)) await list.jumpTo(loc, 'edit') let bufname = await nvim.call('bufname', ['%']) expect(bufname).toMatch('sources.test.ts') }) }) describe('createAction()', () => { it('should overwrite action', async () => { let idx: number let list = new OptionList() listItems.push({ label: 'foo', location: Location.create('untitled:///1', Range.create(0, 0, 0, 0)) }) list.createAction({ name: 'foo', execute: () => { idx = 0 } }) list.createAction({ name: 'foo', execute: () => { idx = 1 } }) disposables.push(manager.registerList(list)) await manager.start(['--normal', 'option']) await manager.session.ui.ready await manager.doAction('foo') expect(idx).toBe(1) }) }) describe('preview()', () => { beforeEach(() => { let list = new SimpleList() disposables.push(manager.registerList(list)) }) async function doPreview(opts: PreviewOptions): Promise { previewOptions = opts await manager.start(['--normal', 'simple']) await manager.session.ui.ready await manager.doAction('preview') let res = await nvim.call('coc#list#has_preview') as number expect(res).toBeGreaterThan(0) let winid = await nvim.call('win_getid', [res]) as number return winid } it('should preview lines', async () => { await doPreview({ filetype: '', lines: ['foo', 'bar'] }) }) it('should preview with bufname', async () => { await doPreview({ bufname: 't.js', filetype: 'typescript', lines: ['foo', 'bar'] }) }) it('should preview with range highlight', async () => { let winid = await doPreview({ bufname: 't.js', filetype: 'typescript', lines: ['foo', 'bar'], range: Range.create(0, 0, 0, 3) }) let res = await nvim.call('getmatches', [winid]) as any[] expect(res.length).toBeGreaterThan(0) }) }) describe('previewLocation()', () => { it('should preview sketch buffer', async () => { await nvim.command('new') await nvim.setLine('foo') let doc = await workspace.document expect(doc.uri).toMatch('untitled') let list = new OptionList() listItems.push({ label: 'foo', location: Location.create(doc.uri, Range.create(0, 0, 0, 0)) }) disposables.push(manager.registerList(list)) await manager.start(['option']) await manager.session.ui.ready await helper.wait(30) await manager.doAction('preview') await nvim.command('wincmd p') let win = await nvim.window let isPreview = await win.getVar('previewwindow') expect(isPreview).toBe(1) let line = await nvim.line expect(line).toBe('foo') }) }) }) describe('list sources', () => { beforeEach(async () => { await nvim.setVar('coc_jump_locations', locations) }) describe('locations', () => { it('should highlight ranges', async () => { await manager.start(['--normal', '--auto-preview', 'location']) await manager.session.ui.ready await helper.waitFor('winnr', ['$'], 3) manager.prompt.cancel() await nvim.command('wincmd k') let name = await nvim.eval('bufname("%")') expect(name).toMatch('sources.test.ts') let res = await nvim.call('getmatches') as any[] expect(res.length).toBe(1) }) it('should not use filename when current buffer only', async () => { let filepath = path.join(os.tmpdir(), 'b7d9e548-00ec-4419-98a8-dc03874e405c') let doc = await helper.createDocument(filepath) let locations = [{ filename: filepath, bufnr: doc.bufnr, lnum: 1, col: 1, text: 'multiple' }, { filename: filepath, bufnr: doc.bufnr, lnum: 1, col: 1, end_lnum: 2, end_col: 1, text: 'multiple' }] await nvim.setVar('coc_jump_locations', locations) await manager.start(['--normal', '--auto-preview', 'location']) await manager.session.ui.ready }) it('should change highlight on cursor move', async () => { await manager.start(['--normal', '--auto-preview', 'location']) await manager.session.ui.ready await nvim.command('exe 2') let bufnr = await nvim.eval('bufnr("%")') await events.fire('CursorMoved', [bufnr, [2, 1]]) await helper.waitFor('winnr', ['$'], 3) await nvim.command('wincmd k') let res = await nvim.call('getmatches') as any expect(res.length).toBe(1) expect(res[0]['pos1']).toEqual([3, 1, 6]) }) it('should highlight multiple line range', async () => { await manager.start(['--normal', '--auto-preview', 'location']) await manager.session.ui.ready await nvim.command('exe 3') let bufnr = await nvim.eval('bufnr("%")') await events.fire('CursorMoved', [bufnr, [2, 1]]) await helper.waitFor('winnr', ['$'], 3) await nvim.command('wincmd k') let res = await nvim.call('getmatches') as any expect(res.length).toBe(1) expect(res[0]['pos1']).toBeDefined() expect(res[0]['pos2']).toBeDefined() }) it('should do open action', async () => { global.formatFilepath = function() { return '' } await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.doAction('open') let name = await nvim.eval('bufname("%")') expect(name).toMatch('sources.test.ts') global.formatFilepath = undefined }) it('should do quickfix action', async () => { await nvim.setVar('coc_quickfix_open_command', 'copen', false) await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.session.ui.selectAll() await manager.doAction('quickfix') let buftype = await nvim.eval('&buftype') expect(buftype).toBe('quickfix') }) it('should do refactor action', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.doAction('refactor') let name = await nvim.eval('bufname("%")') expect(name).toMatch('coc_refactor') }) it('should do tabe action', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.doAction('tabe') let tabs = await nvim.tabpages expect(tabs.length).toBe(2) }) it('should do drop action', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.doAction('drop') let name = await nvim.eval('bufname("%")') expect(name).toMatch('sources.test.ts') }) it('should do vsplit action', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.doAction('vsplit') let name = await nvim.eval('bufname("%")') expect(name).toMatch('sources.test.ts') }) it('should do split action', async () => { await manager.start(['--normal', 'location']) await manager.session.ui.ready await manager.doAction('split') let name = await nvim.eval('bufname("%")') expect(name).toMatch('sources.test.ts') }) }) describe('commands', () => { it('should do run action', async () => { await manager.start(['commands']) await manager.session?.ui.ready await manager.doAction() }) it('should load commands source', async () => { let registry = Registry.as(extension.Extensions.ExtensionContribution) registry.registerExtension('single', { name: 'single', directory: os.tmpdir(), onCommands: ['cmd', 'cmd'], commands: [{ command: 'cmd', title: 'title' }] }) await manager.start(['commands']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) await manager.doAction('append') let line = await helper.getCmdline() expect(line).toMatch(':CocCommand') registry.unregistExtension('single') }) }) describe('diagnostics', () => { function createDiagnostic(msg: string, range?: Range, severity?: DiagnosticSeverity, code?: number): Diagnostic { range = range ? range : Range.create(0, 0, 0, 1) return Diagnostic.create(range, msg, severity || DiagnosticSeverity.Error, code) } async function createDocument(name?: string): Promise { let doc = await helper.createDocument(name) let collection = diagnosticManager.create('test') disposables.push({ dispose: () => { collection.clear() collection.dispose() } }) let diagnostics: Diagnostic[] = [] await doc.buffer.setLines(['foo bar foo bar', 'foo bar', 'foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) diagnostics.push(createDiagnostic('error', Range.create(0, 2, 0, 4), DiagnosticSeverity.Error, 1001)) diagnostics.push(createDiagnostic('warning', Range.create(0, 5, 0, 6), DiagnosticSeverity.Warning, 1002)) diagnostics.push(createDiagnostic('information', Range.create(1, 0, 1, 1), DiagnosticSeverity.Information, 1003)) diagnostics.push(createDiagnostic('hint', Range.create(1, 2, 1, 3), DiagnosticSeverity.Hint, 1004)) diagnostics.push(createDiagnostic('error', Range.create(2, 0, 2, 2), DiagnosticSeverity.Error, 1005)) collection.set(doc.uri, diagnostics) await doc.synchronize() return doc } it('should get label', async () => { let item: DiagnosticItem = { code: 1000, col: 0, end_col: 1, end_lnum: 1, file: os.tmpdir(), level: 0, lnum: 1, location: Location.create('file:///1', Range.create(0, 0, 0, 1)), message: 'message', severity: 'error', source: 'source' } expect(convertToLabel(item, process.cwd(), false).indexOf('1000')).toBe(-1) expect(convertToLabel(item, process.cwd(), true, 'hidden').includes('[source 1000]')).toBe(true) }) it('should load diagnostics source', async () => { await createDocument('a') await createDocument('b') await manager.start(['diagnostics']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let buf = await nvim.buffer let lines = await buf.lines expect(lines.length).toEqual(10) }) it('should filter diagnostics', async () => { await createDocument('list/workspace-folder1/a') await createDocument('list/workspace-folder1/b') await createDocument('list/workspace-folder2/c') await createDocument('list/workspace-folder2/d') const workspaceFolder = path.join(__dirname, 'workspace-folder1') let list = new DiagnosticsList(manager, false) { let res = await list.filterDiagnostics({}) expect(res.length).toBe(20) let spy = jest.spyOn(workspace, 'getWorkspaceFolder').mockReturnValue({ name: 'workspace-folder1', uri: URI.file(workspaceFolder).toString() }) res = await list.filterDiagnostics({ 'workspace-folder': true }) expect(res.length).toBe(10) spy.mockRestore() spy = jest.spyOn(workspace, 'getWorkspaceFolder').mockReturnValue(undefined) res = await list.filterDiagnostics({ 'workspace-folder': true }) expect(res.length).toBe(20) spy.mockRestore() } { let res = await list.filterDiagnostics({ buffer: true }) expect(res.length).toBe(5) } { let res = await list.filterDiagnostics({ level: 'error' }) expect(res.length).toBe(8) } }) it('should refresh on diagnostics refresh', async () => { let doc = await createDocument('bar') await manager.start(['diagnostics']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let diagnostics: Diagnostic[] = [] let collection = diagnosticManager.create('test') diagnostics.push(createDiagnostic('error', Range.create(0, 0, 0, 2), DiagnosticSeverity.Error, 1000)) diagnostics.push(createDiagnostic('error', Range.create(2, 0, 2, 2), DiagnosticSeverity.Error, 1009)) collection.set(doc.uri, diagnostics) let buf = await nvim.buffer await helper.waitValue(async () => { let n = await buf.length return n > 1 }, true) }) }) describe('extensions', () => { it('should load extensions source', async () => { let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(path.join(folder, 'foo'), { recursive: true }) fs.mkdirSync(path.join(folder, 'bar'), { recursive: true }) let infos: ExtensionInfo[] = [] infos.push({ id: 'foo', version: '1.0.0', description: 'foo', root: path.join(folder, 'foo'), exotic: false, state: 'activated', isLocal: false, isLocked: true, packageJSON: { name: 'foo', engines: {} } }) infos.push({ id: 'bar', version: '1.0.0', description: 'bar', root: path.join(folder, 'bar'), exotic: false, state: 'activated', isLocal: true, isLocked: false, packageJSON: { name: 'bar', engines: {} } }) let spy = jest.spyOn(extensions, 'getExtensionStates').mockImplementation(() => { return Promise.resolve(infos) }) const doAction = async (name: string, item: any) => { let action = source.actions.find(o => o.name == name) await action.execute(item) } let states = new ExtensionStat(folder) let manager = new ExtensionManager(states, folder) let source = new ExtensionList(manager) let items = await source.loadItems() expect(items.length).toBe(2) items[0].data.state = 'disabled' await doAction('toggle', items[0]) await doAction('toggle', items[1]) items[1].data.state = 'loaded' await expect(async () => { await doAction('toggle', items[1]) }).rejects.toThrow(Error) await doAction('configuration', items[0]) let jsonfile = path.join(folder, 'bar/package.json') fs.writeFileSync(jsonfile, '{}', 'utf8') await doAction('configuration', items[1]) fs.writeFileSync(jsonfile, '{"contributes": {}}', 'utf8') await doAction('configuration', items[1]) await helper.mockFunction('coc#ui#open_url', 0) await doAction('open', items[1]) await doAction('disable', items[0]) await doAction('disable', items[1]) await doAction('enable', items[0]) await doAction('enable', items[1]) await doAction('lock', items[0]) await expect(async () => { await doAction('reload', items[0]) }).rejects.toThrow(Error) await doAction('uninstall', items) await doAction('help', items[0]) let helpfile = path.join(folder, 'bar/readme.md') fs.writeFileSync(helpfile, '', 'utf8') await doAction('help', items[1]) let bufname = await nvim.eval('bufname("%")') expect(bufname).toMatch('readme') source.doHighlight() spy.mockRestore() }) }) describe('folders', () => { it('should load folders source', async () => { await helper.createDocument(__filename) let uid = uuid() let source = new FolderList() const doAction = async (name: string, item: any) => { let action = source.actions.find(o => o.name == name) await action.execute(item) } let res = await source.loadItems() expect(res.length).toBe(1) await doAction('delete', res[0]) expect(workspace.folderPaths.length).toBe(0) let p = doAction('edit', res[0]) await helper.waitFor('mode', [], 'c') await nvim.input('') await p p = doAction('edit', res[0]) await helper.waitFor('mode', [], 'c') await nvim.input('') await nvim.input('') await p let spy = jest.spyOn(window, 'requestInput').mockReturnValue(Promise.resolve('')) await doAction('newfile', res[0]) spy.mockRestore() fs.rmSync(path.join(os.tmpdir(), uid), { recursive: true, force: true }) let filepath = path.join(os.tmpdir(), uid, 'bar') spy = jest.spyOn(window, 'requestInput').mockReturnValue(Promise.resolve(filepath)) await doAction('newfile', res[0]) let exists = fs.existsSync(filepath) expect(exists).toBe(true) spy.mockRestore() }) }) describe('lists', () => { it('should load lists source', async () => { await manager.start(['lists']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) await manager.doAction() await helper.waitValue(() => { let s = manager.getSession() return s && s.name !== 'lists' }, true) }) }) describe('outline', () => { it('should load items from provider', async () => { let doc = await workspace.document disposables.push(languages.registerDocumentSymbolProvider([{ language: '*' }], { provideDocumentSymbols: document => { let text = document.getText() let parser = new Parser(text, text.includes('detail')) let res = parser.parse() return Promise.resolve(res) } })) let source = new OutlineList() let context = await createContext({}) let res = await source.loadItems(context, CancellationToken.None) expect(res).toEqual([]) let code = `class myClass { fun1() { } }` await doc.applyEdits([TextEdit.insert(Position.create(0, 0), code)]) res = await source.loadItems(context, CancellationToken.None) expect(res.length).toBe(2) source.doHighlight() }) it('should load items by ctags', async () => { helper.updateConfiguration('list.source.outline.ctagsFiletypes', ['vim']) await nvim.command('edit +setl\\ filetype=vim foo') let doc = await workspace.document expect(doc.filetype).toBe('vim') let source = new OutlineList() let context = await createContext({}) context.args = ['-kind', 'function', '-name', 'name'] let res = await source.loadItems(context, CancellationToken.None) expect(res).toEqual([]) res = await source.loadItems(context, CancellationToken.Cancelled) expect(res).toEqual([]) }) }) describe('services', () => { function createService(name: string): IServiceProvider { let _onServiceReady = new Emitter() // public readonly onServiceReady: Event = this. let service: IServiceProvider = { id: name, name, selector: [{ language: 'vim' }], state: ServiceStat.Initial, start(): Promise { service.state = ServiceStat.Running _onServiceReady.fire() return Promise.resolve() }, dispose(): void { service.state = ServiceStat.Stopped }, stop(): void { service.state = ServiceStat.Stopped }, restart(): void { service.state = ServiceStat.Running _onServiceReady.fire() }, onServiceReady: _onServiceReady.event } disposables.push(services.register(service)) return service } it('should load services source', async () => { createService('foo') createService('bar') await manager.start(['services']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let lines = await nvim.call('getline', [1, '$']) as string[] expect(lines.length).toBe(2) }) it('should toggle service state', async () => { let service = createService('foo') await service.start() await manager.start(['services']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let ses = manager.session expect(ses.name).toBe('services') await ses.doAction('toggle') expect(service.state).toBe(ServiceStat.Stopped) await ses.doAction('toggle') }) }) describe('sources', () => { it('should load sources source', async () => { await manager.start(['sources']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let session = manager.getSession() await session.doAction('open') let bufname = await nvim.call('bufname', '%') expect(bufname).toMatch(/native/) }) it('should toggle source state', async () => { await manager.start(['sources']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let session = manager.getSession() await session.doAction('toggle') await session.doAction('toggle') }) it('should refresh source', async () => { await manager.start(['sources']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let session = manager.getSession() await session.doAction('refresh') }) }) describe('notifications', () => { it('should load notifications history', async () => { await window.showInformationMessage('Info message') await manager.start(['notifications']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) let items = await manager.loadItems('notifications') expect(items.at(-1).label).toContain('INFO'.padEnd(7)) expect(items.at(-1).filterText).toBe('Info message') const action = manager.session.defaultAction expect(action.name).toBe('clear') await manager.session.doAction() items = await manager.loadItems('notifications') expect(items.length).toBe(0) }) it('should load notifications from action', async () => { await window.showInformationMessage('Info message') const res = await helper.doAction('notificationHistory') expect(res.length).toBe(1) expect(res[0].message).toBe('Info message') }) }) describe('symbols', () => { it('should create list item', () => { let source = new SymbolsList() let symbolItem = SymbolInformation.create('root', SymbolKind.Method, Range.create(0, 0, 0, 10), '') let item = source.createListItem('', symbolItem, 'kind', './foo') expect(item).toBeDefined() symbolItem.tags = [SymbolTag.Deprecated] item = source.createListItem('', symbolItem, 'kind', './foo') let highlights = item.ansiHighlights let find = highlights.find(o => o.hlGroup == 'CocDeprecatedHighlight') expect(find).toBeDefined() source.fuzzyMatch.setPattern('a') item = source.createListItem('a', symbolItem, 'kind', './foo') expect(item).toBeDefined() source.fuzzyMatch.setPattern('r') item = source.createListItem('r', symbolItem, 'kind', './foo') highlights = item.ansiHighlights find = highlights.find(o => o.hlGroup == 'CocListSearch') expect(find).toBeDefined() }) it('should resolve item', async () => { let source = new SymbolsList() let res = await source.resolveItem({ label: 'label', data: {} }) expect(res).toBeNull() let haveResult = false let disposable = languages.registerWorkspaceSymbolProvider({ provideWorkspaceSymbols: () => [ SymbolInformation.create('root', SymbolKind.Method, Range.create(0, 0, 0, 10), '') ], resolveWorkspaceSymbol: symbolItem => { symbolItem.location = Location.create('lsp:///1', Range.create(0, 0, 1, 0)) return haveResult ? symbolItem : null } }) disposables.push(disposable) let symbols = await languages.getWorkspaceSymbols('', CancellationToken.None) res = await source.resolveItem({ label: 'label', data: { original: symbols[0] } }) expect(res).toBeNull() haveResult = true symbols[0].location = { uri: 'lsp:///1' } res = await source.resolveItem({ label: 'label', data: { original: symbols[0] } }) expect(Location.is(res.location)).toBe(true) if (Location.is(res.location)) { expect(res.location.uri).toBe('lsp:///1') } }) it('should load items', async () => { let source = new SymbolsList() let context = await createContext({ interactive: true }) await expect(async () => { await source.loadItems(context, CancellationToken.None) }).rejects.toThrow(Error) disposables.push(languages.registerWorkspaceSymbolProvider({ provideWorkspaceSymbols: () => [ SymbolInformation.create('root', SymbolKind.Method, Range.create(0, 0, 0, 10), URI.file(__filename).toString()) ] })) let res = await source.loadItems(context, CancellationToken.Cancelled) expect(res).toEqual([]) context.args = ['-kind', 'function'] res = await source.loadItems(context, CancellationToken.None) expect(res).toEqual([]) context.args = [] helper.updateConfiguration('list.source.symbols.excludes', ['**/*.ts']) res = await source.loadItems(context, CancellationToken.None) expect(res).toEqual([]) helper.updateConfiguration('list.source.symbols.excludes', []) res = await source.loadItems(context, CancellationToken.None) expect(res.length).toBe(1) }) it('should load symbols source', async () => { await helper.createDocument() disposables.push(languages.registerWorkspaceSymbolProvider({ provideWorkspaceSymbols: () => [] })) await manager.start(['--interactive', 'symbols']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) }) }) describe('links', () => { it('should load links source', async () => { let disposable = languages.registerDocumentLinkProvider([{ scheme: 'file' }, { scheme: 'untitled' }], { provideDocumentLinks: () => { return [ DocumentLink.create(Range.create(0, 0, 0, 5), 'file:///foo'), DocumentLink.create(Range.create(1, 0, 1, 5), 'file:///bar') ] } }) await manager.start(['links']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) await manager.doAction('jump') disposable.dispose() }) it('should resolve target', async () => { let disposable = languages.registerDocumentLinkProvider([{ scheme: 'file' }, { scheme: 'untitled' }], { provideDocumentLinks: () => { return [ DocumentLink.create(Range.create(0, 0, 0, 5)), ] }, resolveDocumentLink: link => { link.target = 'file:///foo' return link } }) await manager.start(['links']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) await manager.doAction('open') disposable.dispose() }) }) }) ================================================ FILE: src/__tests__/list/ui.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { EventEmitter } from 'events' import { Disposable } from 'vscode-languageserver-protocol' import BasicList from '../../list/basic' import events from '../../events' import manager from '../../list/manager' import { ListItem, IList, ListTask } from '../../list/types' import { disposeAll } from '../../util' import helper from '../helper' let labels: string[] = [] let lastItem: string class SimpleList extends BasicList { public name = 'simple' constructor() { super() this.addAction('open', item => { lastItem = item.label }) } public loadItems(): Promise { return Promise.resolve(labels.map(s => { return { label: s, ansiHighlights: [{ span: [0, 1], hlGroup: 'MoreMsg' }] } as ListItem })) } } class SlowTask extends EventEmitter implements ListTask { private interval: NodeJS.Timeout constructor() { super() let i = 0 let interval = this.interval = setInterval(() => { i++ this.emit('data', { label: i.toString(), highlights: { spans: [[0, 1]], hlGroup: 'Search' } }) if (i == 5) { this.emit('end') clearInterval(interval) } }, 50) } public dispose(): void { clearInterval(this.interval) this.removeAllListeners() } } let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) manager.reset() await helper.reset() }) describe('list ui', () => { describe('selectLines()', () => { it('should select lines', async () => { labels = ['foo', 'bar'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['simple']) let ui = manager.session.ui await ui.ready await ui.selectLines(3, 1) let buf = await nvim.buffer let res = await buf.getSigns({ group: 'coc-list' }) expect(res.length).toBe(2) }) }) describe('preselect', () => { it('should select preselect item', async () => { let list: IList = { actions: [{ name: 'open', execute: () => {} }], name: 'preselect', defaultAction: 'open', loadItems: () => { return Promise.resolve([{ label: 'foo' }, { label: 'bar', preselect: true }]) } } disposables.push(manager.registerList(list)) await manager.start(['--tab', 'preselect']) let ui = manager.session.ui await ui.ready ui.restoreWindow() let line = await nvim.line expect(line).toBe('bar') }) }) describe('resume()', () => { it('should resume with selected lines', async () => { labels = ['foo', 'bar'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['simple']) let ui = manager.session.ui await ui.ready await ui.selectLines(1, 2) await nvim.call('coc#window#close', [ui.winid]) await helper.wait(100) await manager.session.resume() await helper.wait(100) let buf = await nvim.buffer let res = await buf.getSigns({ group: 'coc-list' }) expect(res.length).toBe(2) }) }) describe('events', () => { async function mockMouse(winid: number, lnum: number): Promise { await nvim.command(`let v:mouse_winid = ${winid}`) await nvim.command(`let v:mouse_lnum = ${lnum}`) await nvim.command('let v:mouse_col = 1') } it('should fire action on double click', async () => { labels = ['foo', 'bar'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['simple']) let ui = manager.session.ui await ui.ready await mockMouse(ui.winid, 1) await manager.session.onMouseEvent('<2-LeftMouse>') await helper.waitValue(() => lastItem, 'foo') }) it('should select clicked line', async () => { labels = ['foo', 'bar'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['simple']) let ui = manager.session.ui ui.updateItem(undefined, 0) ui.setLines([], 0, 0) await ui.onMouse('mouseDown') await ui.ready await mockMouse(ui.winid, 2) await ui.onMouse('mouseDrag') await ui.onMouse('mouseUp') await ui.onMouse('mouseDown') await mockMouse(ui.winid, 2) await ui.onMouse('mouseUp') let item = await ui.item await ui.appendItems([]) expect(item.label).toBe('bar') }) it('should jump to original window on click', async () => { labels = ['foo', 'bar'] let win = await nvim.window disposables.push(manager.registerList(new SimpleList())) await manager.start(['simple']) let ui = manager.session.ui await ui.ready await mockMouse(win.id, 1) await ui.onMouse('mouseUp') await helper.wait(50) let curr = await nvim.window expect(curr.id).toBe(win.id) }) it('should highlights items on CursorMoved', async () => { labels = (new Array(400)).fill('a') disposables.push(manager.registerList(new SimpleList())) await manager.start(['--normal', 'simple']) let ui = manager.session.ui await ui.ready await nvim.call('cursor', [350, 1]) await events.fire('CursorMoved', [ui.bufnr, [350, 1]]) let buf = nvim.createBuffer(ui.bufnr) await helper.waitValue(async () => { let res = await buf.getHighlights('list') return res.length > 300 }, true) }) }) }) describe('reversed list', () => { it('should render and add highlights', async () => { labels = ['a', 'b', 'c', 'd'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['--reverse', 'simple']) let ui = manager.session.ui await ui.ready let buf = nvim.createBuffer(ui.bufnr) let lines = await buf.lines expect(lines).toEqual(['d', 'c', 'b', 'a']) await helper.listInput('a') await helper.wait(50) lines = await buf.lines expect(lines).toEqual(['a']) let res = await buf.getHighlights('list') expect(res.length).toBe(2) let win = nvim.createWindow(ui.winid) let height = await win.height expect(height).toBe(1) }) it('should moveUp and moveDown', async () => { labels = ['a', 'b', 'c', 'd'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['--reverse', 'simple']) let ui = manager.session.ui await ui.ready await ui.moveCursor(-1) await helper.waitFor('line', ['.'], 3) await ui.moveCursor(1) await helper.waitFor('line', ['.'], 4) }) it('should toggle selection', async () => { labels = ['a', 'b', 'c', 'd'] disposables.push(manager.registerList(new SimpleList())) await manager.start(['--reverse', '--normal', 'simple']) let ui = manager.session.ui await ui.ready await ui.toggleSelection() let items = ui.selectedItems expect(items.length).toBeGreaterThan(0) expect(items[0].label).toBe('a') let lnum = await nvim.call('line', ['.']) expect(lnum).toBe(3) await helper.listInput('j') await ui.toggleSelection() items = ui.selectedItems expect(items.length).toBe(0) }) it('should prepend list items', async () => { let o: any let p = new Promise(resolve => { let list: IList = { actions: [{ name: 'open', execute: item => { o = item } }], name: 'slow', defaultAction: 'open', loadItems: () => { let task = new SlowTask() task.on('end', () => { resolve(undefined) }) return Promise.resolve(task) } } disposables.push(manager.registerList(list)) void manager.start(['--reverse', '--normal', 'slow']) }) let ui = manager.session.ui ui.setCursor(99) await p await helper.wait(50) // ui.setCursor(2) let buf = nvim.createBuffer(ui.bufnr) let lines = await buf.lines expect(lines).toEqual(['5', '4', '3', '2', '1']) let lnum = await nvim.call('line', ['.']) expect(lnum).toBe(5) }) }) ================================================ FILE: src/__tests__/list/worker.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import styles from 'ansi-styles' import { EventEmitter } from 'events' import { CancellationToken, Disposable } from 'vscode-languageserver-protocol' import BasicList from '../../list/basic' import manager from '../../list/manager' import { ListContext, ListItem, ListTask } from '../../list/types' import { convertItemLabel, indexOf, parseInput, toInputs } from '../../list/worker' import { disposeAll } from '../../util' import helper from '../helper' let items: ListItem[] = [] class DataList extends BasicList { public name = 'data' public loadItems(): Promise { return Promise.resolve(items) } } class EmptyList extends BasicList { public name = 'empty' public loadItems(): Promise { let emitter: any = new EventEmitter() setTimeout(() => { emitter.emit('end') }, 20) return emitter } } class IntervalTaskList extends BasicList { public name = 'task' public timeout = 3000 public loadItems(_context: ListContext, token: CancellationToken): Promise { let emitter: any = new EventEmitter() let i = 0 let interval = setInterval(() => { emitter.emit('data', { label: i.toFixed() }) i++ }, 20) emitter.dispose = () => { clearInterval(interval) emitter.emit('end') } token.onCancellationRequested(() => { emitter.dispose() }) return emitter } } class DelayTask extends BasicList { public name = 'delay' public interactive = true public loadItems(_context: ListContext, token: CancellationToken): Promise { let emitter: any = new EventEmitter() let disposed = false setTimeout(() => { if (disposed) return emitter.emit('data', { label: 'ahead' }) }, 10) setTimeout(() => { if (disposed) return emitter.emit('data', { label: 'abort' }) }, 20) emitter.dispose = () => { disposed = true emitter.emit('end') } token.onCancellationRequested(() => { emitter.dispose() }) return emitter } } class InteractiveList extends BasicList { public name = 'test' public interactive = true public loadItems(context: ListContext, _token: CancellationToken): Promise { return Promise.resolve([{ label: styles.magenta.open + (context.input || '') + styles.magenta.close }]) } } class ErrorList extends BasicList { public name = 'error' public interactive = true public loadItems(_context: ListContext, _token: CancellationToken): Promise { return Promise.reject(new Error('test error')) } } class ErrorTaskList extends BasicList { public name = 'task' public loadItems(_context: ListContext, _token: CancellationToken): Promise { let emitter: any = new EventEmitter() let timeout = setTimeout(() => { emitter.emit('error', new Error('task error')) }, 100) emitter.dispose = () => { clearTimeout(timeout) } return emitter } } let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) manager.reset() await helper.reset() }) describe('util', () => { it('should get index', () => { expect(indexOf('Abc', 'a', true, false)).toBe(0) expect(indexOf('Abc', 'A', false, false)).toBe(0) expect(indexOf('abc', 'A', false, true)).toBe(0) }) it('should parse input with space', () => { let res = parseInput('a b') expect(res).toEqual(['a', 'b']) res = parseInput('a b ') expect(res).toEqual(['a', 'b']) res = parseInput('ab ') expect(res).toEqual(['ab']) }) it('should parse input with escaped space', () => { let res = parseInput('a\\ b') expect(res).toEqual(['a b']) }) it('should convert item label', () => { expect(convertItemLabel({ label: 'foo\nbar\nx' }).label).toBe('foo') const redOpen = '\x1B[31m' const redClose = '\x1B[39m' let label = redOpen + 'foo' + redClose expect(convertItemLabel({ label }).label).toBe('foo') }) it('should convert input', () => { expect(toInputs('foo bar', false)).toEqual(['foo bar']) }) }) describe('list worker', () => { it('should work with long running task', async () => { disposables.push(manager.registerList(new IntervalTaskList())) await manager.start(['task']) await manager.session.worker.drawItems() await manager.session.ui.ready await helper.waitValue(() => { return manager.session?.length > 2 }, true) await manager.cancel() }) it('should sort by sortText', async () => { items = [{ label: 'abc', sortText: 'b' }, { label: 'ade', sortText: 'a' }] disposables.push(manager.registerList(new DataList())) await manager.start(['data']) await manager.session.ui.ready await helper.listInput('a') await helper.waitFor('getline', ['.'], 'ade') await manager.cancel() }) it('should ready with undefined result', async () => { items = undefined disposables.push(manager.registerList(new DataList())) await manager.start(['data']) await manager.session.ui.ready await manager.cancel() }) it('should show empty line for empty task', async () => { disposables.push(manager.registerList(new EmptyList())) await manager.start(['empty']) await manager.session.ui.ready let line = await nvim.call('getline', [1]) expect(line).toMatch('No results') await manager.cancel() }) it('should cancel task by use CancellationToken', async () => { disposables.push(manager.registerList(new IntervalTaskList())) await manager.start(['task']) expect(manager.session?.worker.isLoading).toBe(true) await helper.listInput('1') await helper.wait(50) manager.session?.stop() expect(manager.session?.worker.isLoading).toBe(false) }) it('should render slow interactive list', async () => { disposables.push(manager.registerList(new DelayTask())) await manager.start(['delay']) await helper.listInput('a') await helper.waitFor('getline', [2], 'abort') }) it('should work with interactive list', async () => { disposables.push(manager.registerList(new InteractiveList())) await manager.start(['-I', 'test']) await manager.session?.ui.ready expect(manager.isActivated).toBe(true) await helper.listInput('f') await helper.listInput('a') await helper.listInput('x') await helper.waitFor('getline', ['.'], 'fax') await manager.cancel(true) }) it('should not activate on load error', async () => { disposables.push(manager.registerList(new ErrorList())) await manager.start(['test']) expect(manager.isActivated).toBe(false) }) it('should deactivate on task error', async () => { disposables.push(manager.registerList(new ErrorTaskList())) await manager.start(['task']) await helper.waitValue(() => { return manager.isActivated }, false) }) }) ================================================ FILE: src/__tests__/markdown/index.test.ts ================================================ import { getHighlightItems, toFiletype, parseMarkdown, parseDocuments } from '../../markdown/index' import { Documentation } from '../../types' describe('getHighlightItems', () => { it('should convert filetype', () => { expect(toFiletype(undefined)).toBe('txt') expect(toFiletype('ts')).toBe('typescript') expect(toFiletype('js')).toBe('javascript') expect(toFiletype('bash')).toBe('sh') }) it('should get highlights in single line', () => { let res = getHighlightItems('this line has highlights', 0, [10, 15]) expect(res).toEqual([{ colStart: 10, colEnd: 15, lnum: 0, hlGroup: 'CocFloatActive' }]) }) it('should get highlights when active end extended', () => { let res = getHighlightItems('this line', 0, [5, 30]) expect(res).toEqual([{ colStart: 5, colEnd: 9, lnum: 0, hlGroup: 'CocFloatActive' }]) }) it('should get highlights across line', () => { let res = getHighlightItems('this line\nhas highlights', 0, [5, 15]) expect(res).toEqual([{ colStart: 5, colEnd: 9, lnum: 0, hlGroup: 'CocFloatActive' }, { colStart: 0, colEnd: 5, lnum: 1, hlGroup: 'CocFloatActive' }]) res = getHighlightItems('a\nb\nc\nd', 0, [2, 5]) expect(res).toEqual([ { colStart: 0, colEnd: 1, lnum: 1, hlGroup: 'CocFloatActive' }, { colStart: 0, colEnd: 1, lnum: 2, hlGroup: 'CocFloatActive' }, { colStart: 0, colEnd: 0, lnum: 3, hlGroup: 'CocFloatActive' } ]) }) }) describe('parseMarkdown', () => { it('should parse code blocks', () => { let content = ` \`\`\`js var global = globalThis \`\`\` \`\`\`ts let str:string \`\`\` \`\`\`bash if \`\`\` ` let res = parseMarkdown(content, {}) expect(res.lines).toEqual([ 'var global = globalThis', '', 'let str:string', '', 'if' ]) expect(res.codes).toEqual([ { filetype: 'javascript', startLine: 0, endLine: 1 }, { filetype: 'typescript', startLine: 2, endLine: 3 }, { filetype: 'sh', startLine: 4, endLine: 5 }, ]) }) it('should merge empty lines', () => { let content = ` ![img](http://img.io) ![img](http://img.io) [link](http://example.com) [link](javascript:void(0)) ` let res = parseMarkdown(content, { excludeImages: true }) expect(res.lines).toEqual([ 'link', '', 'link: http://example.com' ]) }) it('should parse html code block', () => { let content = ` example: \`\`\`html
code
\`\`\` ` let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['example:', '
code
']) expect(res.codes).toEqual([{ filetype: 'html', startLine: 1, endLine: 2 }]) }) it('should merge empty lines', async () => { let content = ` https://baidu.com/%25E0%25A4%25A foo bar ` let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['foo', '', 'bar']) }) it('should compose empty lines', () => { let content = 'foo\n\n\nbar\n\n\n' let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['foo', '', 'bar']) }) it('should merge lines', () => { let content = 'first\nsecond' let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['first', 'second']) }) it('should parse ansi highlights', () => { let content = '__foo__\n[link](link)' let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['foo', 'link']) expect(res.highlights).toEqual([ { hlGroup: 'CocBold', lnum: 0, colStart: 0, colEnd: 3 }, { hlGroup: 'CocUnderline', lnum: 1, colStart: 0, colEnd: 4 } ]) }) it('should exclude images by option', () => { let content = 'head\n![img](img)\ncontent ![img](img) ![img](img)' let res = parseMarkdown(content, { excludeImages: false }) expect(res.lines).toEqual(['head', '![img](img)', 'content ![img](img) ![img](img)']) content = 'head\n![img](img)\ncontent ![img](img) ![img](img)' res = parseMarkdown(content, { excludeImages: true }) expect(res.lines).toEqual(['head', 'content']) }) it('should render hr', () => { let content = 'foo\n***\nbar' let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['foo', '───', 'bar']) }) it('should render deleted text', () => { let content = '~foo~' let res = parseMarkdown(content, {}) expect(res.highlights).toEqual([ { hlGroup: 'CocStrikeThrough', lnum: 0, colStart: 0, colEnd: 3 } ]) }) it('should render br', () => { let content = 'a \nb' let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['a', 'b']) }) it('should render code span', () => { let content = '`foo`' let res = parseMarkdown(content, {}) expect(res.highlights).toEqual([ { hlGroup: 'CocMarkdownCode', lnum: 0, colStart: 0, colEnd: 3 } ]) }) it('should render html', () => { let content = '
foo
' let res = parseMarkdown(content, {}) expect(res.lines).toEqual(['foo']) }) it('should render checkbox', () => { let content = '- [x] first\n- [ ] second' let res = parseMarkdown(content, {}) expect(res.lines).toEqual([ ' * [X] first', ' * [ ] second' ]) }) it('should render numbered list', () => { let content = '1. one\n2. two\n3. three' let res = parseMarkdown(content, {}) expect(res.lines).toEqual([ ' 1. one', ' 2. two', ' 3. three' ]) }) it('should render nested list', () => { let content = '- foo\n- bar\n - one\n - two' let res = parseMarkdown(content, {}) expect(res.lines).toEqual([ ' * foo', ' * bar', ' * one', ' * two' ]) }) it('should render complicated nested list', () => { let content = ` - greeting - hello - me you them - hi - me you them - him her - bye - code \`\`\`typescript function foo () { console.log('foo') console.log('bar') } \`\`\` ` let res = parseMarkdown(content, {}) expect(res.lines).toEqual( ` * greeting * hello * me you them * hi * me you them * him her * bye * code function foo () { console.log('foo') console.log('bar') }`.split('\n')) }) }) describe('parseDocuments', () => { it('should parse documents with diagnostic filetypes', () => { let docs = [{ filetype: 'Error', content: 'Error text' }, { filetype: 'Warning', content: 'Warning text' }] let res = parseDocuments(docs) expect(res.lines).toEqual([ 'Error text', '─', 'Warning text' ]) expect(res.codes).toEqual([ { hlGroup: 'CocErrorFloat', startLine: 0, endLine: 1 }, { hlGroup: 'CocWarningFloat', startLine: 2, endLine: 3 } ]) }) it('should parse markdown document with filetype document', () => { let docs = [{ filetype: 'typescript', content: 'const workspace' }, { filetype: 'markdown', content: '**header**' }] let res = parseDocuments(docs) expect(res.lines).toEqual([ 'const workspace', '─', 'header' ]) expect(res.highlights).toEqual([{ colEnd: -1, colStart: 0, hlGroup: "CocFloatDividingLine", lnum: 1, }, { hlGroup: 'CocBold', lnum: 2, colStart: 0, colEnd: 6 }]) expect(res.codes).toEqual([ { filetype: 'typescript', startLine: 0, endLine: 1 } ]) }) it('should parse document with highlights', () => { let docs: Documentation[] = [{ filetype: 'txt', content: 'foo' }, { filetype: 'txt', content: 'foo bar', highlights: [{ lnum: 0, colStart: 4, colEnd: 7, hlGroup: 'String' }] }] let res = parseDocuments(docs) let { highlights } = res expect(highlights[1]).toEqual({ lnum: 2, colStart: 4, colEnd: 7, hlGroup: 'String' }) }) it('should parse documents with active highlights', () => { let docs = [{ filetype: 'javascript', content: 'func(foo, bar)', active: [5, 8] }, { filetype: 'javascript', content: 'func()', active: [15, 20] }] let res = parseDocuments(docs as any) expect(res.highlights[0]).toEqual({ colStart: 5, colEnd: 8, lnum: 0, hlGroup: 'CocFloatActive' }) }) }) ================================================ FILE: src/__tests__/markdown/renderer.test.ts ================================================ import { marked } from 'marked' import Renderer, { bulletPointLine, fixHardReturn, generateTableRow, identify, numberedLine, toSpaces, toSpecialSpaces } from '../../markdown/renderer' import * as styles from '../../markdown/styles' import { parseAnsiHighlights, AnsiResult } from '../../util/ansiparse' marked.setOptions({ renderer: new Renderer(), hooks: Renderer.hooks, }) function parse(text: string): AnsiResult { let m = marked(text) let res = parseAnsiHighlights(m.split(/\n/)[0], true) return res } describe('styles', () => { it('should add styles', () => { let keys = ['gray', 'magenta', 'bold', 'underline', 'italic', 'strikethrough', 'yellow', 'green', 'blue'] for (let key of keys) { let res = styles[key]('text') expect(res).toContain('text') } }) }) describe('Renderer of marked', () => { it('should convert', () => { expect(identify(' ', '')).toBe('') expect(fixHardReturn('a\rb', true)).toBe('a\nb') expect(toSpaces('ab')).toBe(' ') expect(toSpecialSpaces('ab')).toBe('\0\0\0\0\0\0') expect(bulletPointLine(' ', ' * foo')).toBe(' * foo') expect(bulletPointLine(' ', 'foo')).toBe('\0\0\0\0\0\0foo') expect(bulletPointLine(' ', '\0\0\0foo')).toBe('\0\0\0foo') expect(generateTableRow('')).toEqual([]) expect(numberedLine(' ', 'foo', 1).line).toBe(' foo') }) it('should create bold highlights', () => { let res = parse('**note**.') expect(res.highlights[0]).toEqual({ span: [0, 4], hlGroup: 'CocBold' }) }) it('should create italic highlights', () => { let res = parse('_note_.') expect(res.highlights[0]).toEqual({ span: [0, 4], hlGroup: 'CocItalic' }) }) it('should create underline highlights for link', () => { let res = parse('[baidu](https://baidu.com)') expect(res.highlights[0]).toEqual({ span: [0, 5], hlGroup: 'CocMarkdownLink' }) res = parse('https://baidu.com') expect(res.highlights[0]).toEqual({ span: [0, 17], hlGroup: 'CocUnderline' }) res = parse('https://baidu.com/%25E0%25A4%25A') expect(res.line).toBe('') }) it('should parse link', () => { // let res = parse('https://doc.rust-lang.org/nightly/core/iter/traits/iterator/Iterator.t.html#map.v') // console.log(JSON.stringify(res, null, 2)) let link = 'https://doc.rust-lang.org/nightly/core/iter/traits/iterator/Iterator.t.html#map.v' let parsed = marked(link) let res = parseAnsiHighlights(parsed.split(/\n/)[0], true) expect(res.line).toEqual(link) expect(res.highlights.length).toBeGreaterThan(0) expect(res.highlights[0].hlGroup).toBe('CocUnderline') }) it('should create highlight for code span', () => { let res = parse('`let foo = "bar"`') expect(res.highlights[0]).toEqual({ span: [0, 15], hlGroup: 'CocMarkdownCode' }) }) it('should create header highlights', () => { let res = parse('# header') expect(res.highlights[0]).toEqual({ span: [0, 6], hlGroup: 'CocMarkdownHeader' }) res = parse('## header') expect(res.highlights[0]).toEqual({ span: [0, 6], hlGroup: 'CocMarkdownHeader' }) res = parse('### header') expect(res.highlights[0]).toEqual({ span: [0, 6], hlGroup: 'CocMarkdownHeader' }) }) it('should indent blockquote', () => { let res = parse('> header') expect(res.line).toBe(' header') }) it('should parse image', async () => { let res = parse('![title](http://www.baidu.com)') expect(res.line).toMatch('baidu') }) it('should preserve code block', () => { let text = '``` js\nconsole.log("foo")\n```' let m = marked(text) expect(m.split('\n')).toEqual([ '``` js', 'console.log("foo")', '```', '' ]) }) it('should renderer table', () => { let text = ` | Syntax | Description | | ----------- | ----------- | | Header | Title | | Paragraph | Text | ` let res = marked(text) expect(res).toContain('Syntax') }) }) ================================================ FILE: src/__tests__/memos.json ================================================ {} ================================================ FILE: src/__tests__/modules/attach.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import events from '../../events' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { let plugin = await helper.setup(false) nvim = plugin.nvim nvim.emit('notification', 'updateConfig', ['suggest.timeout', 300]) nvim.emit('notification', 'action_not_exists', []) let spy = jest.spyOn(console, 'error').mockImplementation(() => { // noop }) await plugin.init('') spy.mockRestore() }) afterEach(() => { disposeAll(disposables) }) afterAll(async () => { await helper.shutdown() }) describe('notifications', () => { it('should notification before plugin ready', () => { nvim.emit('notification', 'VimEnter', ['']) let timeout = workspace.getConfiguration('suggest').get('timeout') expect(timeout).toBe(300) }) it('should do Log', () => { nvim.emit('notification', 'Log', []) nvim.emit('notification', 'redraw', []) }) it('should do notifications', async () => { nvim.emit('notification', 'listNames', []) let called = false let spy = jest.spyOn(console, 'error').mockImplementation(() => { called = true }) nvim.emit('notification', 'name_not_exists', []) nvim.emit('notification', 'MenuInput', []) await helper.waitValue(() => { return called }, true) spy.mockRestore() }) }) describe('request', () => { it('should get results', async () => { let result nvim.emit('request', 'listNames', [], { send: res => { result = res } }) await helper.waitValue(() => { return Array.isArray(result) }, true) }) it('should return error when plugin not ready', async () => { let plugin = helper.plugin Object.assign(plugin, { ready: false }) let isErr nvim.emit('request', 'listNames', [], { send: (_res, isError) => { isErr = isError } }) await helper.waitValue(() => { return isErr }, true) Object.assign(plugin, { ready: true }) }) it('should not throw when plugin method not found', async () => { let err nvim.emit('request', 'NotExists', [], { send: res => { err = res } }) await helper.waitValue(() => { return typeof err === 'string' }, true) }) it('should echo error instead of throw for autocmds request', async () => { let disposable = events.on('CursorHold', async () => { throw new Error('my error') }) let s = jest.spyOn(events, 'fire').mockImplementation(() => { return Promise.reject(new Error('my error')) }) nvim.call('coc#rpc#request', ['CocAutocmd', ['CursorHold', 1, [1, 1]]], true) let spy = jest.spyOn(nvim, 'echoError').mockImplementation(() => { called = true }) let called = false await helper.waitValue(() => { return called }, true) disposable.dispose() s.mockRestore() spy.mockRestore() }) }) describe('attach', () => { it('should not throw on event handler error', async () => { events.on('CursorHold', () => { throw new Error('error') }) let called = false nvim.emit('request', 'CocAutocmd', ['CursorHold'], { send: () => { called = true } }) await helper.waitValue(() => { return called }, true) }) }) ================================================ FILE: src/__tests__/modules/chars.test.ts ================================================ import { CancellationTokenSource, Range } from 'vscode-languageserver-protocol' import { Chars, IntegerRanges, detectLanguage, getCharCode, parseSegments, sameScope, splitKeywordOption } from '../../model/chars' import { makeLine } from '../helper' describe('funcs', () => { it('should splitKeywordsOptions', () => { expect(splitKeywordOption('')).toEqual([]) expect(splitKeywordOption('_,-,128-140,#-43')).toEqual(['_', '-', '128-140', '#-43']) expect(splitKeywordOption('^a-z,#,^')).toEqual(['^a-z', '#', '^']) expect(splitKeywordOption('@,^a-z')).toEqual(['@', '^a-z']) expect(splitKeywordOption('48-57,,,_')).toEqual(['48-57', ',', '_']) expect(splitKeywordOption(' -~,^,,9')).toEqual([' -~', '^,', '9']) expect(splitKeywordOption(' -~,^,')).toEqual([' -~', '^,']) }) it('should toCharCode', () => { expect(getCharCode('10')).toBe(10) expect(getCharCode('')).toBeUndefined() expect(getCharCode('a')).toBe(97) }) it('should sameScope', () => { expect(sameScope(1, 3)).toBe(true) expect(sameScope(266, 1024)).toBe(true) expect(sameScope(97, 19970)).toBe(false) }) it('should use Segmenter', () => { let res = Array.from(parseSegments('你好世界', 'cn')) expect(Array.isArray(res)).toBe(true) let fn = Intl['Segmenter'] if (typeof fn === 'function') { Object.defineProperty(Intl, 'Segmenter', { get: () => { return undefined } }) res = Array.from(parseSegments('你好世界', 'cn')) Object.defineProperty(Intl, 'Segmenter', { get: () => { return fn } }) expect(res).toEqual(['你好世界']) res = Array.from(parseSegments('你好世界', '')) expect(res).toBeDefined() } }) it('should delete language', () => { expect(detectLanguage('你'.charCodeAt(0))).toBe('cn') expect(detectLanguage('れ'.charCodeAt(0))).toBe('ja') expect(detectLanguage('것'.charCodeAt(0))).toBe('ko') expect(detectLanguage(0xFFFF)).toBe('') }) }) describe('IntegerRanges', () => { it('should add ranges', () => { let r = new IntegerRanges() expect(r.flatten()).toEqual([]) r.add(4, 3) r.add(1) r.add(2) expect(r.flatten()).toEqual([1, 1, 2, 2, 3, 4]) r.add(2, 7) expect(r.flatten()).toEqual([1, 1, 2, 7]) r.add(7, 9) expect(r.flatten()).toEqual([1, 1, 2, 9]) r.add(2, 5) expect(r.flatten()).toEqual([1, 1, 2, 9]) }) it('should exclude ranges', () => { let r = new IntegerRanges() r.add(1, 2) r.add(4, 6) r.exclude(3, 3) r.exclude(8) r.exclude(9, 10) expect(r.flatten()).toEqual([1, 2, 4, 6]) r.exclude(4, 6) r.exclude(1, 2) expect(r.flatten()).toEqual([]) r.add(3, 8) r.exclude(1, 3) r.exclude(8, 9) expect(r.flatten()).toEqual([4, 7]) r.exclude(6, 5) expect(r.flatten()).toEqual([4, 4, 7, 7]) expect(r.includes(4)).toBe(true) expect(r.includes(7)).toBe(true) }) it('should check word code', () => { let r = new IntegerRanges([], true) expect(r.includes(258)).toBe(true) expect(r.includes(894)).toBe(false) expect(r.includes(33)).toBe(false) }) it('should fromKeywordOption', () => { let r = IntegerRanges.fromKeywordOption('@,_') expect(r.includes(97)).toBe(true) expect(r.includes('_'.charCodeAt(0))).toBe(true) r = IntegerRanges.fromKeywordOption('@-@,9,^') expect(r.includes(9)).toBe(true) expect(r.includes('@'.charCodeAt(0))).toBe(true) expect(r.includes('^'.charCodeAt(0))).toBe(true) r = IntegerRanges.fromKeywordOption('@,^a-z') expect(r.includes(97)).toBe(false) r = IntegerRanges.fromKeywordOption('48-57,,,_') expect(r.includes(48)).toBe(true) expect(r.includes(','.charCodeAt(0))).toBe(true) expect(r.includes('_'.charCodeAt(0))).toBe(true) r = IntegerRanges.fromKeywordOption('_,-,128-140,#-43') expect(r.includes(130)).toBe(true) expect(r.includes(43)).toBe(true) expect(r.includes('_'.charCodeAt(0))).toBe(true) expect(r.includes('-'.charCodeAt(0))).toBe(true) expect(r.includes('#'.charCodeAt(0))).toBe(true) r = IntegerRanges.fromKeywordOption(' -~,^,,9') expect(r.includes(' '.charCodeAt(0))).toBe(true) expect(r.includes(','.charCodeAt(0))).toBe(false) expect(r.includes(9)).toBe(true) r = IntegerRanges.fromKeywordOption('65,-x,x-') expect(r.includes(65)).toBe(true) r = IntegerRanges.fromKeywordOption('128-140,-') expect(r.includes('-'.charCodeAt(0))).toBe(true) }) }) describe('chars', () => { describe('isKeywordChar()', () => { it('should match @', () => { let chars = new Chars('@') expect(chars.isKeywordChar('a')).toBe(true) expect(chars.isKeywordChar('z')).toBe(true) expect(chars.isKeywordChar('A')).toBe(true) expect(chars.isKeywordChar('Z')).toBe(true) expect(chars.isKeywordChar('\u205f')).toBe(false) }) it('should iterateWords', async () => { let chars = new Chars('@') let res = Array.from(chars.iterateWords(' 你好foo bar')) expect(res).toEqual([[1, 3], [3, 6], [7, 10]]) }) it('should match code range', () => { let chars = new Chars('48-57') expect(chars.isKeywordChar('0')).toBe(true) expect(chars.isKeywordChar('9')).toBe(true) }) it('should match @-@', () => { let chars = new Chars('@-@') expect(chars.isKeywordChar('@')).toBe(true) }) it('should match single code', () => { let chars = new Chars('58') expect(chars.isKeywordChar(':')).toBe(true) }) it('should match single character', () => { let chars = new Chars('_') expect(chars.isKeywordChar('_')).toBe(true) }) }) describe('addKeyword()', () => { it('should add keyword', () => { let chars = new Chars('_') chars.addKeyword(':') expect(chars.isKeywordChar(':')).toBe(true) chars.addKeyword(':') expect(chars.isKeywordChar(':')).toBe(true) }) }) describe('computeWordRanges()', () => { it('should computeWordRanges', async () => { let chars = new Chars('@') let res = await chars.computeWordRanges(['abc def hijkl'], Range.create(0, 4, 0, 7)) expect(res).toEqual({ def: [ { start: { line: 0, character: 4 }, end: { line: 0, character: 7 } } ] }) res = await chars.computeWordRanges(['abc def ', 'foo def', ' ', ' abc'], Range.create(0, 3, 4, 0)) expect(Object.keys(res)).toEqual(['def', 'foo', 'abc']) const r = (sl, sc, el, ec) => { return Range.create(sl, sc, el, ec) } expect(res['def']).toEqual([r(0, 4, 0, 7), r(1, 4, 1, 7)]) expect(res['foo']).toEqual([r(1, 0, 1, 3)]) expect(res['abc']).toEqual([r(3, 1, 3, 4)]) }) it('should wait after timeout', async () => { let l = makeLine(200) let arr: string[] = [] for (let i = 0; i < 8000; i++) { arr.push(l) } let chars = new Chars('@') let tokenSource = new CancellationTokenSource() let timer = setTimeout(() => { tokenSource.cancel() }, 30) await chars.computeWordRanges(arr, Range.create(0, 0, 8000, 0), tokenSource.token) clearTimeout(timer) expect(tokenSource.token.isCancellationRequested).toBe(true) }) }) describe('matchLine()', () => { it('should matchLine', async () => { let text = 'a'.repeat(2048) let chars = new Chars('@') expect(chars.matchLine(text, 'cn', 3, 128)).toEqual(['a'.repeat(128)]) expect(chars.matchLine('a b c')).toEqual([]) expect(chars.matchLine('foo bar')).toEqual(['foo', 'bar']) expect(chars.matchLine('?foo bar')).toEqual(['foo', 'bar']) expect(chars.matchLine('?foo $')).toEqual(['foo']) expect(chars.matchLine('?foo foo foo')).toEqual(['foo']) expect(chars.matchLine(' 你好foo')).toEqual(['你好', 'foo']) expect(chars.matchLine('bar你好', 'cn')).toEqual(['bar', '你好']) expect(chars.matchLine('foo😍bar foo,bar')).toEqual(['foo', 'bar']) expect(chars.matchLine('你好世界', '')).toBeDefined() }) }) describe('iskeyword()', () => { it('should check isKeyword', () => { let chars = new Chars('@') expect(chars.isKeyword('foo')).toBe(true) expect(chars.isKeyword('f@')).toBe(false) }) }) }) ================================================ FILE: src/__tests__/modules/cursors.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import commands from '../../commands' import Cursors from '../../cursors' import CursorsSession, { surroundChanges } from '../../cursors/session' import TextRange from '../../cursors/textRange' import { getChange, getDelta, getVisualRanges, isSurroundChange, isTextChange, splitRange, SurroundChange, TextChange } from '../../cursors/util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let cursors: Cursors let ns: number beforeAll(async () => { await helper.setup() nvim = helper.nvim ns = await nvim.createNamespace('coc-cursors') cursors = window.cursors }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { nvim.pauseNotification() cursors.reset() await nvim.resumeNotification() await helper.reset() }) async function rangeCount(): Promise { let buf = await nvim.buffer let markers = await buf.getExtMarks(ns, 0, -1) return markers.length } describe('cursors utils', () => { describe('getDelta()', () => { it('should get delta count', async () => { expect(getDelta({ prepend: [1, 'foo'], append: [1, 'bar'], remove: false })).toBe(4) expect(getDelta({ offset: 0, remove: 2, insert: 'foo' })).toBe(1) }) }) describe('surroundChanges()', () => { it('should check surround changes', async () => { expect(surroundChanges([], 0)).toBe(false) expect(surroundChanges([{ offset: 1, add: 'f' }, { offset: 3, add: 'f' }], 0)).toBe(false) }) it('should get surround change', async () => { const getText = (newText: string): string => { let r = new TextRange(0, 0, 'foo') let res = getChange(r, Range.create(0, 0, 0, 3), newText) as SurroundChange expect(isSurroundChange(res)).toBe(true) r.applySurroundChange(res) return r.text } expect(getText('"foo"')).toBe('"foo"') expect(getText('o')).toBe('o') expect(getText('')).toBe('') }) }) describe('getChange()', () => { it('should get end change', async () => { const getText = (character: number, newText: string) => { let start = Position.create(0, character) let r = new TextRange(0, 0, 'foo') let res = getChange(r, Range.create(start, r.range.end), newText) as TextChange expect(isTextChange(res)).toBe(true) r.applyTextChange(res) return r.text } expect(getText(3, 'bar')).toBe('foobar') expect(getText(1, '')).toBe('f') expect(getText(2, 'ba')).toBe('foba') }) it('should get normal change', async () => { const getText = (start: number, end: number, newText: string) => { let r = new TextRange(0, 0, 'foo') let res = getChange(r, Range.create(0, start, 0, end), newText) as TextChange expect(isTextChange(res)).toBe(true) r.applyTextChange(res) return r.text } expect(getText(0, 0, 'a')).toBe('afoo') expect(getText(0, 1, '')).toBe('oo') expect(getText(0, 2, 'ba')).toBe('bao') }) it('should split ranges', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\n\nend')]) let ranges = splitRange(doc, Range.create(0, 3, 3, 0)) expect(ranges).toEqual([Range.create(1, 0, 1, 3)]) }) it('should get visual ranges', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\nend')]) let ranges = getVisualRanges(doc, Range.create(0, 3, 3, 0)) expect(ranges.length).toBe(4) }) }) }) describe('cursors', () => { describe('cancel()', () => { it('should cancel cursors session', async () => { cursors.cancel(999) let doc = await workspace.document cursors.cancel(doc.bufnr) await nvim.call('setline', [1, ['a', 'b']]) await nvim.call('cursor', [1, 1]) await doc.synchronize() await cursors.select(doc.bufnr, 'position', 'n') let activated = await cursors.isActivated() expect(activated).toBe(true) cursors.cancel(doc.bufnr) activated = await cursors.isActivated() expect(activated).toBe(false) }) it('should cancel when no have ranges', async () => { let doc = await workspace.document let session = cursors.createSession(doc) session.checkRanges() let activated = await cursors.isActivated() expect(activated).toBe(false) session.cancel() session.dispose() }) }) describe('select()', () => { it('should throw with unsupported kind', async () => { let doc = await workspace.document let fn = async () => { await cursors.select(doc.bufnr, 'undefined', 'n') } await expect(fn()).rejects.toThrow(/not supported/) }) it('should select by position', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['a', 'b']]) await nvim.call('cursor', [1, 1]) await doc.synchronize() await cursors.select(doc.bufnr, 'position', 'n') let n = await rangeCount() expect(n).toBe(1) await nvim.setOption('virtualedit', 'onemore') await nvim.call('cursor', [2, 2]) await cursors.select(doc.bufnr, 'position', 'n') n = await rangeCount() expect(n).toBe(2) await cursors.select(doc.bufnr, 'position', 'n') n = await rangeCount() expect(n).toBe(1) }) it('should select by word', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo', 'bar']]) await nvim.call('cursor', [1, 1]) await doc.synchronize() await cursors.select(doc.bufnr, 'word', 'n') let n = await rangeCount() expect(n).toBe(1) await nvim.call('cursor', [2, 2]) await cursors.select(doc.bufnr, 'word', 'n') n = await rangeCount() expect(n).toBe(2) await cursors.select(doc.bufnr, 'word', 'n') n = await rangeCount() expect(n).toBe(1) }) it('should toggle select', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo', 'bar']]) await nvim.call('cursor', [1, 1]) await doc.synchronize() await cursors.select(doc.bufnr, 'word', 'n') let n = await rangeCount() expect(n).toBe(1) await cursors.select(doc.bufnr, 'word', 'n') n = await rangeCount() expect(n).toBe(0) let activated = await doc.buffer.getVar('coc_cursors_activated') expect(activated).toBe(0) }) it('should select last character', async () => { let doc = await workspace.document await nvim.setOption('virtualedit', 'onemore') await nvim.call('setline', [1, ['}', '{']]) await nvim.call('cursor', [1, 2]) await doc.synchronize() await cursors.select(doc.bufnr, 'word', 'n') let n = await rangeCount() expect(n).toBe(1) await nvim.call('cursor', [2, 1]) await doc.synchronize() await cursors.select(doc.bufnr, 'word', 'n') n = await rangeCount() expect(n).toBe(2) }) it('should select by visual range', async () => { let doc = await workspace.document await cursors.select(doc.bufnr, 'range', 'v') let activated = await cursors.isActivated() expect(activated).toBe(false) await nvim.call('setline', [1, ['"foo"', '"bar"']]) await nvim.call('cursor', [1, 1]) await nvim.command('normal! vE') await doc.synchronize() await cursors.select(doc.bufnr, 'range', 'v') let n = await rangeCount() expect(n).toBe(1) await nvim.call('cursor', [2, 1]) await nvim.command('normal! vE') await cursors.select(doc.bufnr, 'range', 'v') n = await rangeCount() expect(n).toBe(2) await cursors.select(doc.bufnr, 'range', 'v') n = await rangeCount() expect(n).toBe(1) }) it('should select visual blocks', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['let x = "foo"', 'let y = "bar"']]) await doc.synchronize() await nvim.call('cursor', [1, 1]) await nvim.input('') await nvim.input('je') await helper.wait(30) await cursors.select(doc.bufnr, 'range', '\x16') let n = await rangeCount() expect(n).toBe(2) }) it('should select by operator char type', async () => { await nvim.command('nmap x (coc-cursors-operator)') let bufnr = await nvim.call('bufnr', ['%']) as number await nvim.call('setline', [1, ['"short"', '"long"']]) await nvim.call('cursor', [1, 2]) await nvim.input('xi"') await helper.waitValue(() => { let s = cursors.getSession(bufnr) return s ? s.currentRanges.length : 0 }, 1) }) it('should select by operator line type', async () => { await nvim.command('nmap x (coc-cursors-operator)') let bufnr = await nvim.call('bufnr', ['%']) as number await nvim.call('setline', [1, ['"short"', '"long"']]) await nvim.call('cursor', [1, 2]) await nvim.input('xap') await helper.waitValue(() => { let s = cursors.getSession(bufnr) return s ? s.currentRanges.length : 0 }, 2) }) }) describe('addRanges()', () => { it('should add ranges', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo foo foo', 'bar bar']]) await doc.synchronize() let ranges = [ Range.create(0, 0, 0, 3), Range.create(0, 4, 0, 7), Range.create(0, 8, 0, 11), Range.create(1, 0, 1, 3), Range.create(1, 4, 1, 7) ] await commands.executeCommand('editor.action.addRanges', ranges) let n = await rangeCount() expect(n).toBe(5) }) }) describe('validChange()', () => { it('should check valid change', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo', 'foo', '']]) await doc.synchronize() let ranges = [ Range.create(0, 0, 0, 3), Range.create(1, 0, 1, 3), ] await helper.doAction('addRanges', ranges) let session = cursors.getSession(doc.bufnr) expect(session.validChange(Range.create(0, 0, 1, 0), '')).toBe(false) expect(session.validChange(Range.create(0, 0, 2, 0), '\n\n')).toBe(false) expect(session.validChange(Range.create(1, 0, 1, 3), 'bar')).toBe(false) }) }) describe('onChange()', () => { let session: CursorsSession function edit(sl: number, sc: number, el: number, ec: number, text: string): TextEdit { let r = Range.create(sl, sc, el, ec) return TextEdit.replace(r, text) } async function assertEdits(edits: TextEdit[], characters: number[], line?: string) { let doc = await workspace.document await nvim.call('setline', [1, ['foo foo foo', '']]) await doc.synchronize() let ranges = [ Range.create(0, 0, 0, 3), Range.create(0, 4, 0, 7), Range.create(0, 8, 0, 11), ] await cursors.addRanges(ranges) session = cursors.getSession(doc.bufnr) let p = new Promise(resolve => { let disposable = session.onDidUpdate(() => { disposable.dispose() resolve(undefined) }) void doc.applyEdits(edits) }) await p if (line != null) { expect(doc.getline(0)).toBe(line) } let arr: number[] = [] session.currentRanges.forEach(r => { arr.push(r.start.character, r.end.character) }) expect(arr).toEqual(characters) session.cancel() } it('should adjust on text insert', async () => { await assertEdits([edit(0, 0, 0, 0, 'bar\n')], [0, 3, 4, 7, 8, 11]) await assertEdits([edit(0, 0, 0, 0, 'b')], [0, 4, 5, 9, 10, 14], 'bfoo bfoo bfoo') await assertEdits([edit(0, 1, 0, 1, 'b')], [0, 4, 5, 9, 10, 14], 'fboo fboo fboo') await assertEdits([edit(0, 3, 0, 3, 'b')], [0, 4, 5, 9, 10, 14], 'foob foob foob') await assertEdits([edit(0, 3, 0, 4, '\n')], [0, 3, 0, 3, 4, 7], 'foo') await assertEdits([edit(1, 0, 1, 0, 'bar')], [0, 3, 4, 7, 8, 11]) await nvim.call('setline', [1, ['foo foo foo', '']]) await nvim.call('cursor', [1, 4]) await assertEdits([edit(0, 8, 0, 8, 'b')], [0, 4, 5, 9, 10, 14], 'bfoo bfoo bfoo') let col = await nvim.call('col', ['.']) expect(col).toBe(5) }) it('should adjust on text delete', async () => { await assertEdits([edit(0, 2, 0, 3, '')], [0, 2, 3, 5, 6, 8], 'fo fo fo') await assertEdits([edit(0, 3, 0, 4, '')], [0, 3, 3, 6, 7, 10], 'foofoo foo') await assertEdits([edit(0, 4, 0, 7, '')], [0, 0, 1, 1, 2, 2], ' ') await nvim.setLine('foo foo') await nvim.call('cursor', [1, 4]) await assertEdits([edit(0, 3, 0, 7, '')], [0, 3, 4, 7], 'foo foo') await assertEdits([edit(0, 1, 0, 11, '')], [], 'f') }) it('should adjust on text change', async () => { await assertEdits([edit(0, 0, 0, 0, '"'), edit(0, 3, 0, 3, '"')], [0, 5, 6, 11, 12, 17], '"foo" "foo" "foo"') await assertEdits([edit(0, 0, 0, 1, 'b')], [0, 3, 4, 7, 8, 11], 'boo boo boo') await assertEdits([edit(0, 0, 0, 3, 'ba')], [0, 2, 3, 5, 6, 8], 'ba ba ba') await nvim.call('setline', [1, ['', '']]) await nvim.call('cursor', [2, 1]) await assertEdits([edit(0, 4, 0, 5, 'ba')], [0, 4, 5, 9, 10, 14], 'baoo baoo baoo') let col = await nvim.call('col', ['.']) expect(col).toBe(1) }) it('should adjust on range remove', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo', 'foobar']]) await doc.synchronize() let ranges = [Range.create(0, 0, 0, 3), Range.create(1, 0, 1, 6)] await cursors.addRanges(ranges) session = cursors.getSession(doc.bufnr) await doc.applyEdits([TextEdit.del(Range.create(0, 0, 0, 3))]) await doc.synchronize() let lines = await doc.buffer.lines expect(lines).toEqual(['', '']) session.cancel() }) it('should adjust on undo & redo', async () => { let doc = await workspace.document let edits = [edit(0, 0, 0, 0, '"'), edit(0, 3, 0, 3, '"')] await nvim.call('setline', [1, ['foo foo foo', '']]) await doc.synchronize() let ranges = [ Range.create(0, 0, 0, 3), Range.create(0, 4, 0, 7), Range.create(0, 8, 0, 11), ] await cursors.addRanges(ranges) session = cursors.getSession(doc.bufnr) let p = new Promise(resolve => { let disposable = session.onDidUpdate(() => { disposable.dispose() resolve(undefined) }) void doc.applyEdits(edits) }) await p await nvim.command('undo') await helper.waitValue(() => { return nvim.getLine() }, 'foo foo foo') expect(session.currentRanges).toEqual(ranges) }) it('should highlight on empty content change', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo', '']]) await doc.synchronize() let ranges = [Range.create(0, 0, 0, 3)] await cursors.addRanges(ranges) session = cursors.getSession(doc.bufnr) await nvim.call('setline', [1, ['foo', '']]) await doc.synchronize() let c = await rangeCount() expect(c).toBe(1) }) it('should cancel when insert line break', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['foo', '']]) await doc.synchronize() let ranges = [Range.create(0, 0, 0, 3)] await cursors.addRanges(ranges) session = cursors.getSession(doc.bufnr) await nvim.call('cursor', [1, 2]) await nvim.input('i') await doc.synchronize() let activated = await cursors.isActivated() expect(activated).toBe(false) }) }) describe('applyComposedEdit()', () => { async function setup(): Promise { let doc = await workspace.document await nvim.call('setline', [1, ['bar foo foo', 'foo']]) await doc.synchronize() let session = cursors.createSession(doc) session.addRanges([ Range.create(0, 4, 0, 7), Range.create(0, 8, 0, 11), Range.create(1, 0, 1, 3), ]) return session } it('should check change before first range', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['abc foob foob', 'foob']) expect(res).toBe(false) }) it('should check change of first range', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar foo foob', 'foob']) expect(res).toBe(false) }) it('should check delete exceed range', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar fofoo', 'foo']) expect(res).toBe(false) }) it('should check content prepend', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar bfoo bfoo', 'bfoo']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 8), Range.create(0, 9, 0, 13), Range.create(1, 0, 1, 4), ]) s = await setup() doc = await workspace.document res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar bfoo bfoo', 'xfoo']) expect(res).toBe(false) }) it('should check content insert', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar fboo fboo', 'fboo']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 8), Range.create(0, 9, 0, 13), Range.create(1, 0, 1, 4), ]) }) it('should check content append', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar foob foob', 'foob']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 8), Range.create(0, 9, 0, 13), Range.create(1, 0, 1, 4), ]) }) it('should check content delete #1', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar oo oo', 'oo']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 6), Range.create(0, 7, 0, 9), Range.create(1, 0, 1, 2), ]) }) it('should check content delete #2', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar ', '']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 4), Range.create(0, 5, 0, 5), Range.create(1, 0, 1, 0), ]) }) it('should check content delete #3', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar fo fo', 'fo']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 6), Range.create(0, 7, 0, 9), Range.create(1, 0, 1, 2), ]) }) it('should check content change #1', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar fa fa', 'fa']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 6), Range.create(0, 7, 0, 9), Range.create(1, 0, 1, 2), ]) }) it('should check content change #1', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar fa fa', 'fa']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 6), Range.create(0, 7, 0, 9), Range.create(1, 0, 1, 2), ]) }) it('should check content change #2', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar ab ab', 'ab']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 6), Range.create(0, 7, 0, 9), Range.create(1, 0, 1, 2), ]) }) it('should check content change #3', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar xfa xfa', 'xfa']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 7), Range.create(0, 8, 0, 11), Range.create(1, 0, 1, 3), ]) }) it('should check content change #4', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar xfao xfao', 'xfao']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 8), Range.create(0, 9, 0, 13), Range.create(1, 0, 1, 4), ]) }) it('should check surround add', async () => { let s = await setup() let doc = await workspace.document let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar "foo" "foo"', '"foo"']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 9), Range.create(0, 10, 0, 15), Range.create(1, 0, 1, 5), ]) }) it('should check surround remove', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['bar "foo" "foo"', '"foo"']]) await doc.synchronize() let s = cursors.createSession(doc) s.addRanges([ Range.create(0, 4, 0, 9), Range.create(0, 10, 0, 15), Range.create(1, 0, 1, 5), ]) let res = s.applyComposedEdit(doc.textDocument.lines.slice(), ['bar foo foo', 'foo']) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 7), Range.create(0, 8, 0, 11), Range.create(1, 0, 1, 3), ]) }) it('should check surround change', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['bar "foo" "foo"', '"foo"']]) await doc.synchronize() let s = cursors.createSession(doc) s.addRanges([ Range.create(0, 4, 0, 9), Range.create(0, 10, 0, 15), Range.create(1, 0, 1, 5), ]) let res = s.applyComposedEdit(doc.textDocument.lines.slice(), [`bar 'foo' 'foo'`, `'foo'`]) expect(res).toBe(true) expect(s.currentRanges).toEqual([ Range.create(0, 4, 0, 9), Range.create(0, 10, 0, 15), Range.create(1, 0, 1, 5), ]) }) }) describe('key mappings', () => { async function setup(): Promise { let doc = await workspace.document await nvim.call('setline', [1, ['a', 'b', 'c']]) await doc.synchronize() let session = cursors.createSession(doc) session.addRanges([ Range.create(0, 0, 0, 1), Range.create(1, 0, 1, 1), Range.create(2, 0, 2, 1), ]) } async function hasKeymap(key): Promise { let buf = await nvim.buffer let keymaps = await buf.getKeymap('n') as any return keymaps.find(o => o.lhs == key) != null } it('should setup cancel keymap', async () => { await setup() let count = await rangeCount() expect(count).toBe(3) await nvim.input('') await helper.wait(50) count = await rangeCount() expect(count).toBe(0) let has = await hasKeymap('') expect(has).toBe(false) }) it('should next key wrapscan', async () => { await setup() await nvim.call('cursor', [1, 1]) const next = async (line: number, character: number) => { await nvim.input('') await helper.waitValue(async () => { return await nvim.call('coc#cursor#position') }, [line, character]) } await next(1, 0) await next(2, 0) await next(0, 0) }) it('should previous key wrapscan', async () => { await setup() await nvim.call('cursor', [3, 1]) const prev = async (line: number, character: number) => { await nvim.input('') await helper.waitValue(async () => { return await nvim.call('coc#cursor#position') }, [line, character]) } await prev(1, 0) await prev(0, 0) await prev(2, 0) }) it('should next key no wrapscan', async () => { helper.updateConfiguration('cursors.wrapscan', false) await setup() await nvim.call('cursor', [3, 1]) const next = async (line: number, character: number) => { await nvim.input('') await helper.wait(50) let cursor = await nvim.call('coc#cursor#position') expect(cursor).toEqual([line, character]) } await next(2, 0) }) it('should previous key no wrapscan', async () => { helper.updateConfiguration('cursors.wrapscan', false) await setup() await nvim.call('cursor', [1, 1]) const prev = async (line: number, character: number) => { await nvim.input('') await helper.wait(30) let cursor = await nvim.call('coc#cursor#position') expect(cursor).toEqual([line, character]) } await prev(0, 0) }) }) }) ================================================ FILE: src/__tests__/modules/db.test.ts ================================================ import fs from 'fs' import os from 'os' import path from 'path' import DB from '../../model/db' import Mru from '../../model/mru' const root = fs.mkdtempSync(path.join(os.tmpdir(), 'coc-mru-')) let db: DB beforeAll(async () => { db = new DB(path.join(root, 'db.json')) }) afterAll(async () => { db.destroy() }) afterEach(async () => { db.clear() }) describe('DB', () => { test('db.exists()', () => { let exists = db.exists('a.b') expect(exists).toBe(false) db.push('a.b', { foo: 1 }) exists = db.exists('a.b.foo') expect(exists).toBe(true) }) test('db.load()', () => { fs.rmSync(root, { force: true, recursive: true }) db.clear() expect(db.fetch(undefined)).toEqual({}) }) test('db.fetch()', () => { let res = db.fetch('x') expect(res).toBeUndefined() db.push('x', 1) res = db.fetch('x') expect(res).toBe(1) db.push('x', { foo: 1 }) res = db.fetch('x') expect(res).toEqual({ foo: 1 }) }) test('db.delete()', () => { db.push('foo.bar', 1) db.delete('not_exists') db.delete('foo.bar') let exists = db.exists('foo.bar') expect(exists).toBe(false) }) test('db.push()', () => { db.push('foo.x', 1) db.push('foo.y', '2') db.push('foo.z', true) db.push('foo.n', null) db.push('foo.o', { x: 1 }) let res = db.fetch('foo') expect(res).toEqual({ x: 1, y: '2', z: true, n: null, o: { x: 1 } }) }) }) describe('Mru', () => { it('should load items', async () => { let mru = new Mru('test', root) await mru.clean() let res = await mru.load() expect(res.length).toBe(0) res = mru.loadSync() expect(res.length).toBe(0) }) it('should consider last line break', async () => { let file = path.join(root, 'test') fs.writeFileSync(file, '1\n2\n3\n4\n5\n', 'utf8') let mru = new Mru('test', root) let res = await mru.load() expect(res.length).toBe(5) await mru.clean() }) it('should load sync', async () => { let file = path.join(root, 'test') fs.writeFileSync(file, '\n', 'utf8') let mru = new Mru('test', root) let res = mru.loadSync() expect(res.length).toBe(0) fs.writeFileSync(file, '1\n2\n3\n4\n5\n', 'utf8') res = mru.loadSync() expect(res.length).toBe(5) }) it('should limit lines', async () => { let file = path.join(root, 'test') fs.writeFileSync(file, '1\n2\n3\n4\n5\n', 'utf8') let mru = new Mru('test', root, 3) let lines = await mru.load() expect(lines).toEqual(['1', '2', '3']) await mru.clean() }) it('should add items', async () => { let mru = new Mru('test', root) await mru.add('a') await mru.add('b') let res = await mru.load() expect(res.length).toBe(2) await mru.clean() }) it('should consider BOM', async () => { let mru = new Mru('test', root) let file = path.join(root, 'test') let buf = Buffer.from([239, 187, 191]) fs.writeFileSync(file, buf) await mru.add('item') let res = await mru.load() expect(res.length).toBe(1) }) it('should add when file it does not exist', async () => { let mru = new Mru('test', root) await mru.clean() await mru.add('a') let res = await mru.load() expect(res).toEqual(['a']) }) it('should remove item', async () => { let mru = new Mru('test', root) await mru.add('a') await mru.remove('a') let res = await mru.load() expect(res.length).toBe(0) await mru.clean() }) }) ================================================ FILE: src/__tests__/modules/diagnosticBuffer.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Location, Position, Range, TextEdit } from 'vscode-languageserver-types' import { DiagnosticBuffer } from '../../diagnostic/buffer' import workspace from '../../workspace' import helper from '../helper' import { URI } from 'vscode-uri' let nvim: Neovim async function createDiagnosticBuffer(): Promise { let doc = await workspace.document return new DiagnosticBuffer(nvim, doc) } function createDiagnostic(msg: string, range?: Range, severity?: DiagnosticSeverity, tags?: DiagnosticTag[]): Diagnostic & { collection: string } { range = range ? range : Range.create(0, 0, 0, 1) return Object.assign(Diagnostic.create(range, msg, severity || DiagnosticSeverity.Error, 999, 'test'), { collection: 'test', tags }) } async function getExtmarkers(bufnr: number, ns: number): Promise<[number, number, number, number, string][]> { let res = await nvim.call('nvim_buf_get_extmarks', [bufnr, ns, 0, -1, { details: true }]) as any return res.map(o => { return [o[1], o[2], o[3].end_row, o[3].end_col, o[3].hl_group] }) } let ns: number let virtualTextSrcId: number beforeAll(async () => { await helper.setup() nvim = helper.nvim ns = await nvim.createNamespace('coc-diagnostic') virtualTextSrcId = await nvim.createNamespace('coc-diagnostic-virtualText') }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('diagnostic buffer', () => { describe('showFloat()', () => { it('should not show float when disabled', async () => { helper.updateConfiguration('diagnostic.messageTarget', 'echo') let buf = await createDiagnosticBuffer() let diagnostics = [createDiagnostic('foo')] let res = await buf.showFloat(diagnostics, 'echo') expect(res).toBe(false) }) it('should not show float in insert mode', async () => { let doc = await workspace.document let buf = new DiagnosticBuffer(nvim, doc) await nvim.input('i') let mode = await nvim.mode expect(mode.mode).toBe('i') let diagnostics = [createDiagnostic('foo')] let res = await buf.showFloat(diagnostics) expect(res).toBe(false) }) it('should show related information in floating window', async () => { let buf = await createDiagnosticBuffer() let range = Range.create(0, 0, 0, 10) let location = Location.create(URI.file(__filename).toString(), range) let diagnostic = Diagnostic.create(range, 'msg', 1, 1000, 'test', [{ location, message: 'this is a related information' }]) await buf.showFloat([diagnostic]) await nvim.call('cursor', [1, 1]) let winid = await helper.waitFloat() let win = nvim.createWindow(winid) let floatBuf = await win.buffer let lines = await floatBuf.lines expect(lines.length).toBe(7) expect(lines[2]).toBe('Related information:') expect(lines[4].includes('this is a related information')).toBe(true) }) it('should show formated diagnostics', async () => { helper.updateConfiguration('diagnostic.format', '[%source] %message') let buf = await createDiagnosticBuffer() let diagnostic = createDiagnostic('foo') await buf.showFloat([diagnostic]) await nvim.call('cursor', [1, 1]) let winid = await helper.waitFloat() let win = nvim.createWindow(winid) let floatBuf = await win.buffer let lines = await floatBuf.lines expect(lines[0]).toEqual('[test] foo') }) }) describe('refresh()', () => { it('should not add signs when disabled', async () => { helper.updateConfiguration('diagnostic.enableSign', false) let diagnostics = [createDiagnostic('foo'), createDiagnostic('bar')] let buf = await createDiagnosticBuffer() buf.addSigns('a', diagnostics) await helper.wait(30) let res = await nvim.call('sign_getplaced', [buf.bufnr, { group: 'CocDiagnostica' }]) let signs = res[0].signs expect(signs).toEqual([]) }) it('should filter sign by signLevel', async () => { helper.updateConfiguration('diagnostic.signLevel', 'error') let range = Range.create(0, 0, 0, 3) let diagnostics = [createDiagnostic('foo', range, DiagnosticSeverity.Warning), createDiagnostic('bar', range, DiagnosticSeverity.Warning)] let buf = await createDiagnosticBuffer() buf.addSigns('a', diagnostics) await helper.wait(30) let res = await nvim.call('sign_getplaced', [buf.bufnr, { group: 'CocDiagnostica' }]) let signs = res[0].signs expect(signs).toBeDefined() expect(signs.length).toBe(0) }) it('should set diagnostic info', async () => { let r = Range.create(0, 1, 0, 2) let diagnostics = [ createDiagnostic('foo', r, DiagnosticSeverity.Error), createDiagnostic('bar', r, DiagnosticSeverity.Warning), createDiagnostic('foo', r, DiagnosticSeverity.Hint), createDiagnostic('bar', r, DiagnosticSeverity.Information) ] let buf = await createDiagnosticBuffer() await buf.update('', diagnostics) let buffer = await nvim.buffer let res = await buffer.getVar('coc_diagnostic_info') expect(res).toEqual({ lnums: [1, 1, 1, 1], information: 1, hint: 1, warning: 1, error: 1 }) }) it('should add highlight', async () => { let buf = await createDiagnosticBuffer() let doc = workspace.getDocument(buf.bufnr) await nvim.setLine('abc') await doc.patchChange() nvim.pauseNotification() buf.updateHighlights('', [ createDiagnostic('foo', Range.create(0, 0, 0, 1), DiagnosticSeverity.Error), createDiagnostic('bar', Range.create(0, 0, 0, 1), DiagnosticSeverity.Warning) ]) await nvim.resumeNotification() let markers = await getExtmarkers(buf.bufnr, ns) expect(markers).toEqual([ [0, 0, 0, 1, 'CocWarningHighlight'], [0, 0, 0, 1, 'CocErrorHighlight'] ]) nvim.pauseNotification() buf.updateHighlights('', []) await nvim.resumeNotification() let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, ns, 0, -1, { details: true }]) as any[] expect(res.length).toBe(0) }) it('should add deprecated highlight', async () => { let diagnostic = createDiagnostic('foo', Range.create(0, 0, 0, 1), DiagnosticSeverity.Information, [DiagnosticTag.Deprecated]) let buf = await createDiagnosticBuffer() let doc = workspace.getDocument(buf.bufnr) await nvim.setLine('foo') await doc.patchChange() nvim.pauseNotification() buf.updateHighlights('', [diagnostic]) await nvim.resumeNotification() let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, ns, 0, -1, {}]) as [number, number, number][] expect(res.length).toBe(2) }) it('should not refresh for empty diagnostics', async () => { let buf: any = await createDiagnosticBuffer() let fn = jest.fn() buf.refresh = () => { fn() } buf.update('c', []) expect(fn).toHaveBeenCalledTimes(0) }) it('should refresh when content changes is empty', async () => { let diagnostic = createDiagnostic('foo', Range.create(0, 0, 0, 1), DiagnosticSeverity.Error) let buf = await createDiagnosticBuffer() let doc = workspace.getDocument(buf.bufnr) await nvim.setLine('foo') doc._forceSync() nvim.pauseNotification() buf.updateHighlights('', [diagnostic]) await nvim.resumeNotification() await nvim.setLine('foo') await doc.patchChange() doc._forceSync() let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, ns, 0, -1, { details: true }]) as any expect(res.length).toBe(1) }) }) describe('setDiagnosticInfo()', () => { it('should include lines', async () => { helper.updateConfiguration('diagnostic.virtualTextCurrentLineOnly', false) let buf = await createDiagnosticBuffer() let r = Range.create(1, 1, 1, 3) let diagnostics = [ createDiagnostic('foo', r, DiagnosticSeverity.Information), createDiagnostic('foo', r, DiagnosticSeverity.Information), createDiagnostic('foo', r, DiagnosticSeverity.Hint), createDiagnostic('foo', r, DiagnosticSeverity.Hint), createDiagnostic('foo', r, DiagnosticSeverity.Warning), createDiagnostic('foo', r, DiagnosticSeverity.Warning), ] await buf.update('', diagnostics) let buffer = await nvim.buffer let res = await buffer.getVar("coc_diagnostic_info") as any expect(res.lnums).toEqual([0, 2, 2, 2]) }) }) describe('echoMessage', () => { it('should not echoMessage when disabled', async () => { helper.updateConfiguration('diagnostic.enableMessage', 'never') let buf = await createDiagnosticBuffer() let res = await buf.echoMessage(false, Position.create(0, 0)) res = await buf.echoMessage(true, Position.create(0, 0)) expect(res).toBe(false) }) }) describe('showVirtualText()', () => { beforeEach(() => { helper.updateConfiguration('diagnostic.virtualText', true) }) it('should not show virtualText when disabled', async () => { helper.updateConfiguration('diagnostic.virtualTextCurrentLineOnly', false) let buf = await createDiagnosticBuffer() await buf.setState(false) let diagnostic = createDiagnostic('foo') let diagnostics = [diagnostic] await buf.update('', diagnostics) let res = await buf.showVirtualTextCurrentLine(1) expect(res).toBe(false) helper.updateConfiguration('diagnostic.virtualTextCurrentLineOnly', true) buf.loadConfiguration() await buf.setState(false) res = await buf.showVirtualTextCurrentLine(1) expect(res).toBe(false) }) it('should change format of virtualText message', async () => { helper.updateConfiguration('diagnostic.virtualTextFormat', '%source %message') let buf = await createDiagnosticBuffer() let diagnostic = createDiagnostic('foo') await buf.update('', [diagnostic]) let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, virtualTextSrcId, 0, -1, { details: true }]) as any let texts = res[0][3].virt_text expect(texts[0][0]).toBe(' test foo') }) it('should show virtual text on current line', async () => { let diagnostic = createDiagnostic('foo') let buf = await createDiagnosticBuffer() let diagnostics = [diagnostic] await buf.update('', diagnostics) let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, virtualTextSrcId, 0, -1, { details: true }]) as any expect(res.length).toBe(1) let texts = res[0][3].virt_text expect(texts[0]).toEqual([' foo', 'CocErrorVirtualText']) }) it('should show virtual text at window column', async () => { helper.updateConfiguration('diagnostic.virtualTextWinCol', 90) let diagnostic = createDiagnostic('foo') let buf = await createDiagnosticBuffer() let diagnostics = [diagnostic] await buf.update('', diagnostics) let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, virtualTextSrcId, 0, -1, { details: true }]) as any expect(res.length).toBe(1) let texts = res[0][3].virt_text expect(texts[0]).toEqual([' foo', 'CocErrorVirtualText']) }) it('should virtual text on all lines', async () => { helper.updateConfiguration('diagnostic.virtualTextCurrentLineOnly', false) let buf = await createDiagnosticBuffer() let diagnostics = [ createDiagnostic('foo', Range.create(0, 0, 0, 1)), createDiagnostic('bar', Range.create(1, 0, 1, 1)), ] await buf.update('', diagnostics) let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, virtualTextSrcId, 0, -1, { details: true }]) as any expect(res.length).toBe(2) }) it('should filter by virtualTextLevel', async () => { helper.updateConfiguration('diagnostic.virtualTextLevel', 'error') helper.updateConfiguration('diagnostic.virtualTextAlign', 'after') let buf = await createDiagnosticBuffer() let diagnostics = [ createDiagnostic('foo', Range.create(0, 0, 0, 1), DiagnosticSeverity.Error), createDiagnostic('foo', Range.create(0, 0, 0, 1), DiagnosticSeverity.Warning), createDiagnostic('bar', Range.create(1, 0, 1, 1), DiagnosticSeverity.Warning), ] await buf.update('', diagnostics) let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, virtualTextSrcId, 0, -1, { details: true }]) as any expect(res.length).toBe(1) }) it('should limit virtual text count of one line', async () => { helper.updateConfiguration('diagnostic.virtualTextCurrentLineOnly', false) helper.updateConfiguration('diagnostic.virtualTextLimitInOneLine', 1) let buf = await createDiagnosticBuffer() let diagnostics = [ createDiagnostic('foo', Range.create(0, 0, 0, 1)), createDiagnostic('bar', Range.create(0, 0, 0, 1)), ] await buf.update('', diagnostics) let res = await nvim.call('nvim_buf_get_extmarks', [buf.bufnr, virtualTextSrcId, 0, -1, { details: true }]) as any expect(res[0][3].virt_text.length).toBe(1) }) }) describe('updateLocationList()', () => { beforeEach(() => { helper.updateConfiguration('diagnostic.locationlistUpdate', true) }) it('should update location list', async () => { let buf = await createDiagnosticBuffer() await nvim.call('setloclist', [0, [], 'r', { title: 'Diagnostics of coc', items: [] }]) await buf.update('a', [createDiagnostic('foo')]) let res = await nvim.eval(`getloclist(bufwinid(${buf.bufnr}))`) as any[] expect(res.length).toBe(1) expect(res[0].text).toBe('[test 999] foo [E]') }) }) describe('clear()', () => { beforeEach(() => { helper.updateConfiguration('diagnostic.virtualText', true) }) it('should clear all diagnostics', async () => { let diagnostic = createDiagnostic('foo') let buf = await createDiagnosticBuffer() let diagnostics = [diagnostic] await buf.update('', diagnostics) buf.clear() let buffer = await nvim.buffer let res = await buffer.getVar("coc_diagnostic_info") expect(res == null).toBe(true) }) }) describe('reset()', () => { it('should clear exists diagnostics', async () => { let buf = await createDiagnosticBuffer() let diagnostic = createDiagnostic('foo') let diagnostics = [diagnostic] await buf.update('test', diagnostics) await helper.wait(30) await buf.reset({}) let res = await buf.doc.buffer.getVar("coc_diagnostic_info") as any expect(res?.error).toBe(0) }) it('should not refresh when not enabled', async () => { let buf = await createDiagnosticBuffer() let diagnostic = createDiagnostic('foo') let diagnostics = [diagnostic] await buf.update('test', diagnostics) await buf.setState(false) await buf.setState(false) await buf.reset({ diagnostics: [createDiagnostic('bar')] }) let res = await buf.doc.buffer.getVar("coc_diagnostic_info") as any expect(res).toBeNull() await buf.setState(true) res = await buf.doc.buffer.getVar("coc_diagnostic_info") as any expect(res?.error).toBe(1) }) }) describe('isEnabled()', () => { it('should return false when buffer disposed', async () => { let buf = await createDiagnosticBuffer() await nvim.command(`bd! ${buf.bufnr}`) buf.dispose() let res = await buf.isEnabled() expect(res).toBe(false) let arr = buf.getHighlightItems([]) expect(arr.length).toBe(0) }) }) describe('getHighlightItems()', () => { it('should get highlights', async () => { let buf = await createDiagnosticBuffer() let doc = workspace.getDocument(workspace.bufnr) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) let diagnostics = [ createDiagnostic('one', Range.create(0, 0, 0, 1), DiagnosticSeverity.Warning), createDiagnostic('one', Range.create(0, 1, 0, 2), DiagnosticSeverity.Warning), createDiagnostic('two', Range.create(0, 0, 2, 3), DiagnosticSeverity.Error), createDiagnostic('three', Range.create(1, 0, 1, 2), DiagnosticSeverity.Hint), ] diagnostics[0].tags = [DiagnosticTag.Unnecessary] diagnostics[1].tags = [DiagnosticTag.Deprecated] let res = buf.getHighlightItems(diagnostics) expect(res.length).toBe(7) expect(res.map(o => o.hlGroup)).toEqual([ 'CocUnusedHighlight', 'CocWarningHighlight', 'CocErrorHighlight', 'CocDeprecatedHighlight', 'CocWarningHighlight', 'CocHintHighlight', 'CocErrorHighlight' ]) }) }) describe('getDiagnostics()', () => { it('should get sorted diagnostics', async () => { let buf = await createDiagnosticBuffer() let diagnostics = [ createDiagnostic('three', Range.create(0, 1, 0, 2), DiagnosticSeverity.Error), createDiagnostic('one', Range.create(0, 0, 0, 2), DiagnosticSeverity.Warning), createDiagnostic('two', Range.create(0, 0, 0, 2), DiagnosticSeverity.Error), ] diagnostics[0].tags = [DiagnosticTag.Unnecessary] await buf.reset({ x: diagnostics, y: [createDiagnostic('four', Range.create(0, 0, 0, 2), DiagnosticSeverity.Error)] }) let res = buf.getDiagnosticsAt(Position.create(0, 1), false) let arr = res.map(o => o.message) expect(arr).toEqual(['four', 'two', 'three', 'one']) }) }) }) ================================================ FILE: src/__tests__/modules/diagnosticCollection.test.ts ================================================ import DiagnosticCollection from '../../diagnostic/collection' import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Range } from 'vscode-languageserver-types' function createDiagnostic(msg: string, range?: Range): Diagnostic { range = range ? range : Range.create(0, 0, 0, 1) return Diagnostic.create(range, msg) } describe('diagnostic collection', () => { it('should create collection', () => { let collection = new DiagnosticCollection('test') expect(collection.name).toBe('test') collection.dispose() }) it('should set diagnostic with uri', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let uri = 'file:///1' collection.set(uri, [diagnostic]) expect(collection.get(uri).length).toBe(1) collection.set(uri, []) expect(collection.get(uri).length).toBe(0) }) it('should set severity for hint tags', async () => { let collection = new DiagnosticCollection('test') let diagnostics = [{ range: null, message: undefined, tags: [DiagnosticTag.Deprecated] }, { range: Range.create(0, 0, 0, 1), message: undefined, tags: [DiagnosticTag.Unnecessary] }] let uri = 'file:///1' collection.set(uri, diagnostics) let arr = collection.get(uri) expect(arr.length).toBe(2) expect(arr[0].severity).toBe(DiagnosticSeverity.Hint) expect(arr[1].severity).toBe(DiagnosticSeverity.Hint) }) it('should clear diagnostics with null as diagnostics', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let uri = 'file:///1' collection.set(uri, [diagnostic]) expect(collection.get(uri).length).toBe(1) collection.set(uri, null) expect(collection.get(uri).length).toBe(0) }) it('should clear diagnostics with undefined as diagnostics in entries', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let entries: [string, Diagnostic[] | null][] = [ ['file:1', [diagnostic]], ['file:1', undefined] ] let uri = 'file:///1' collection.set(entries) expect(collection.get(uri).length).toBe(0) }) it('should set diagnostics with entries', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let uri = 'file:///1' let other = 'file:///2' let entries: [string, Diagnostic[]][] = [ [uri, [diagnostic]], [other, [diagnostic]], [uri, [createDiagnostic('other')]] ] collection.set(entries) expect(collection.get(uri).length).toBe(2) expect(collection.get(other).length).toBe(1) }) it('should delete diagnostics for uri', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let uri = 'file:///1' collection.set(uri, [diagnostic]) collection.delete(uri) expect(collection.get(uri).length).toBe(0) }) it('should clear all diagnostics', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let uri = 'file:///1' let fn = jest.fn() collection.set(uri, [diagnostic]) collection.onDidDiagnosticsChange(fn) collection.clear() expect(collection.get(uri).length).toBe(0) expect(fn).toHaveBeenCalledTimes(1) }) it('should call for every uri with diagnostics', () => { let collection = new DiagnosticCollection('test') let diagnostic = createDiagnostic('error') let uri = 'file:///1' let other = 'file:///2' let entries: [string, Diagnostic[]][] = [ [uri, [diagnostic]], [other, [diagnostic]], [uri, [createDiagnostic('other')]] ] collection.set(entries) let arr: string[] = [] collection.forEach(uri => { arr.push(uri) }) expect(arr).toEqual([uri, other]) }) }) ================================================ FILE: src/__tests__/modules/diagnosticManager.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import os from 'os' import path from 'path' import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Location, Position, Range } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import manager from '../../diagnostic/manager' import { getHighlightGroup, getNameFromSeverity, getSeverityName, getSeverityType, severityLevel, sortDiagnostics } from '../../diagnostic/util' import Document from '../../model/document' import window from '../../window' import commands from '../../commands' import workspace from '../../workspace' import fs from 'fs' import helper, { createTmpFile } from '../helper' let nvim: Neovim function createDiagnostic(msg: string, range?: Range, severity?: DiagnosticSeverity): Diagnostic { range = range ? range : Range.create(0, 0, 0, 1) return Diagnostic.create(range, msg, severity || DiagnosticSeverity.Error) } let virtualTextSrcId: number beforeAll(async () => { await helper.setup() nvim = helper.nvim virtualTextSrcId = await nvim.createNamespace('coc-diagnostic-virtualText') }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { manager.reset() await helper.reset() }) async function createDocument(name?: string): Promise { let doc = await helper.createDocument(name) let collection = manager.create('test') let diagnostics: Diagnostic[] = [] await doc.buffer.setLines(['foo bar foo bar', 'foo bar', 'foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() diagnostics.push(createDiagnostic('error', Range.create(0, 2, 0, 4), DiagnosticSeverity.Error)) diagnostics.push(createDiagnostic('warning', Range.create(0, 5, 0, 6), DiagnosticSeverity.Warning)) diagnostics.push(createDiagnostic('information', Range.create(1, 0, 1, 1), DiagnosticSeverity.Information)) diagnostics.push(createDiagnostic('hint', Range.create(1, 2, 1, 3), DiagnosticSeverity.Hint)) diagnostics.push(createDiagnostic('error', Range.create(2, 0, 2, 2), DiagnosticSeverity.Error)) collection.set(doc.uri, diagnostics) await helper.waitValue(() => { let buf = manager.getItem(doc.bufnr) if (!buf.config.autoRefresh) return true return buf.getDiagnosticsAt(Position.create(0, 0), true).length > 0 }, true) return doc } describe('diagnostic manager', () => { describe('defineSigns', () => { it('should defineSigns', () => { manager.defineSigns({ enableHighlightLineNumber: false }) }) }) describe('setLocationlist()', () => { it('should set location list', async () => { let doc = await createDocument() await helper.doAction('fillDiagnostics', doc.bufnr) let res = await nvim.call('getloclist', [doc.bufnr]) as any[] expect(res.length).toBeGreaterThan(2) helper.updateConfiguration('diagnostic.locationlistLevel', 'error') await manager.setLocationlist(doc.bufnr) res = await nvim.call('getloclist', [doc.bufnr]) as any[] expect(res.length).toBe(2) }) it('should throw when buffer not attached', async () => { await nvim.command(`vnew +setl\\ buftype=nofile`) let doc = await workspace.document let fn = async () => { await manager.setLocationlist(doc.bufnr) } await expect(fn()).rejects.toThrow(/not/) }) }) describe('events', () => { it('should delay refresh when buffer visible', async () => { let doc = await helper.createDocument() await nvim.command('edit tmp') let collection = manager.create('foo') let diagnostics: Diagnostic[] = [] await doc.buffer.setLines(['foo bar foo bar', 'foo bar', 'foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() diagnostics.push(createDiagnostic('error', Range.create(0, 2, 0, 4), DiagnosticSeverity.Error)) collection.set(doc.uri, diagnostics) let buf = doc.buffer let val = await buf.getVar('coc_diagnostic_info') as any expect(val == null).toBe(true) let ns = await nvim.createNamespace('coc-diagnosticfoo') let markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBe(0) await nvim.command(`b ${buf.id}`) await helper.waitFor('eval', ['empty(get(b:,"coc_diagnostic_info",{}))'], 0) collection.dispose() }) it('should delay refresh on InsertLeave', async () => { let doc = await workspace.document await nvim.input('i') let collection = manager.create('foo') let diagnostics: Diagnostic[] = [] await doc.buffer.setLines(['foo bar foo bar', 'foo bar', 'foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() diagnostics.push(createDiagnostic('error', Range.create(0, 2, 0, 4), DiagnosticSeverity.Error)) collection.set(doc.uri, diagnostics) let buf = doc.buffer await helper.waitValue(async () => { let val = await buf.getVar('coc_diagnostic_info') as any return val == null }, true) let ns = await nvim.createNamespace('coc-diagnosticfoo') let markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBe(0) await nvim.input('') await helper.waitValue(async () => { let markers = await buf.getExtMarks(ns, 0, -1) return markers.length }, 1) }) it('should show diagnostic virtual text on CursorMoved', async () => { helper.updateConfiguration('diagnostic.virtualText', true) helper.updateConfiguration('diagnostic.virtualTextCurrentLineOnly', true) let doc = await createDocument() await helper.wait(30) let markers = await doc.buffer.getExtMarks(virtualTextSrcId, 0, -1, { details: true }) await manager.toggleDiagnosticBuffer(doc.bufnr) await nvim.call('cursor', [1, 3]) await helper.wait(30) markers = await doc.buffer.getExtMarks(virtualTextSrcId, 0, -1, { details: true }) expect(markers.length).toBe(0) }) }) describe('refresh()', () => { it('should refresh on buffer create', async () => { let uri = URI.file(path.join(path.dirname(__dirname), 'doc')).toString() let fn = jest.fn() let disposable = manager.onDidRefresh(() => { fn() }) let collection = manager.create('tmp') let diagnostic = createDiagnostic('My Error') collection.set(uri, [diagnostic]) let doc = await helper.createDocument('doc') await helper.wait(30) let val = await doc.buffer.getVar('coc_diagnostic_info') as any expect(fn).toHaveBeenCalled() expect(val).toBeDefined() expect(val.error).toBe(1) collection.dispose() disposable.dispose() }) }) describe('toggleDiagnostic()', () => { it('should toggle diagnostics for all buffer', async () => { await createDocument() let doc = await createDocument() await helper.doAction('diagnosticToggle') let item = manager.getItem(doc.bufnr) expect(item.config.enable).toBe(false) await manager.toggleDiagnostic(1) expect(item.config.enable).toBe(true) }) }) describe('getDiagnosticList()', () => { it('should get all diagnostics', async () => { await createDocument() let collection = manager.create('test') let fsPath = await createTmpFile('foo') let doc = await helper.createDocument(fsPath) let diagnostics: Diagnostic[] = [] diagnostics.push(createDiagnostic('error', Range.create(0, 0, 0, 1), DiagnosticSeverity.Error)) diagnostics.push(createDiagnostic('error', Range.create(0, 1, 0, 2), DiagnosticSeverity.Error)) diagnostics.push(createDiagnostic('error', Range.create(0, 2, 0, 3), DiagnosticSeverity.Warning)) collection.set(doc.uri, diagnostics) collection.set('file:///1', []) let list = await helper.doAction('diagnosticList') expect(list).toBeDefined() expect(list.length).toBeGreaterThanOrEqual(5) expect(list[0].severity).toBe('Error') expect(list[1].severity).toBe('Error') expect(list[2].severity).toBe('Error') }) it('should filter diagnostics by configuration', async () => { helper.updateConfiguration('diagnostic.level', 'warning') helper.updateConfiguration('diagnostic.showUnused', false) helper.updateConfiguration('diagnostic.showDeprecated', false) let doc = await createDocument() let buf = manager.getItem(doc.bufnr) let diagnostics = manager.getDiagnostics(buf)['test'] diagnostics[0].tags = [DiagnosticTag.Unnecessary] diagnostics[2].tags = [DiagnosticTag.Deprecated] let list = await manager.getDiagnosticList() expect(list.length).toBe(3) let res = manager.getDiagnostics(buf)['test'] expect(res.length).toBe(1) let ranges = manager.getSortedRanges(doc.uri, buf.config.level) expect(ranges.length).toBe(3) }) it('should load file from disk ', async () => { let fsPath = __filename let collection = manager.create('test') let diagnostics: Diagnostic[] = [] diagnostics.push(createDiagnostic('error', Range.create(0, 0, 0, 1), DiagnosticSeverity.Error)) let uri = URI.file(fsPath).toString() collection.set(uri, diagnostics) let arr: Diagnostic[] = [] arr.push(createDiagnostic('error', Range.create(1, 0, 1, 1), undefined)) collection.set('test:1', arr) let list = await manager.getDiagnosticList() expect(list.length).toBe(2) }) }) describe('preview()', () => { it('should not throw with empty diagnostics', async () => { await helper.doAction('diagnosticPreview') let tabpage = await nvim.tabpage let wins = await tabpage.windows expect(wins.length).toBe(1) }) it('should open preview window', async () => { await createDocument() await nvim.call('cursor', [1, 3]) await manager.preview() let res = await nvim.call('coc#window#find', ['&previewwindow', 1]) expect(res).toBeDefined() }) }) describe('setConfigurationErrors()', () => { it('should set configuration errors on refresh', async () => { let file = path.join(os.tmpdir(), '69075963-48d6-4427-92db-287a09d5e976') fs.writeFileSync(file, ']', 'utf8') workspace.configurations.parseConfigurationModel(file) let errors = workspace.configurations.errors expect(errors.size).toBeGreaterThan(0) let list = await manager.getDiagnosticList() expect(list.length).toBe(1) expect(list[0].file).toBe(file) manager.checkConfigurationErrors() fs.unlinkSync(file) }) }) describe('create()', () => { it('should create diagnostic collection', async () => { let doc = await workspace.document let collection = manager.create('test') collection.set(doc.uri, [createDiagnostic('foo')]) await helper.waitValue(async () => { let info = await doc.buffer.getVar('coc_diagnostic_info') return info != null }, true) }) }) describe('getSortedRanges()', () => { it('should get sorted ranges of document', async () => { let doc = await workspace.document await nvim.call('setline', [1, ['a', 'b', 'c']]) let collection = manager.create('test') let diagnostics: Diagnostic[] = [] diagnostics.push(createDiagnostic('x', Range.create(0, 0, 0, 1))) diagnostics.push(createDiagnostic('y', Range.create(0, 1, 0, 2))) diagnostics.push(createDiagnostic('z', Range.create(1, 0, 1, 2))) collection.set(doc.uri, diagnostics) let item = manager.getItem(doc.bufnr) let level = item.config.level let ranges = manager.getSortedRanges(doc.uri, level) expect(ranges[0]).toEqual(Range.create(0, 0, 0, 1)) expect(ranges[1]).toEqual(Range.create(0, 1, 0, 2)) expect(ranges[2]).toEqual(Range.create(1, 0, 1, 2)) ranges = manager.getSortedRanges(doc.uri, level, 'error') expect(ranges.length).toBe(3) expect(manager.getSortedRanges(doc.uri, level, 'warning').length).toBe(0) }) }) describe('getDiagnosticsInRange', () => { it('should get diagnostics in range', async () => { let doc = await createDocument() let res = manager.getDiagnosticsInRange(doc.textDocument, Range.create(0, 0, 1, 0)) expect(res.length).toBe(3) doc = await helper.createDocument() res = manager.getDiagnosticsInRange(doc.textDocument, Range.create(0, 0, 1, 0)) expect(res.length).toBe(0) }) }) describe('getCurrentDiagnostics', () => { it('should get undefined when buffer not attached', async () => { await nvim.command(`edit +setl\\ buftype=nofile tmp`) let res = await manager.getCurrentDiagnostics() await helper.doAction('diagnosticInfo') expect(res).toBeUndefined() }) it('should get diagnostics under cursor', async () => { await createDocument() let diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBe(0) await nvim.call('cursor', [1, 4]) diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBe(1) helper.updateConfiguration('diagnostic.checkCurrentLine', true) await nvim.call('cursor', [1, 2]) diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBe(2) }) it('should get empty diagnostic at end of line', async () => { let doc = await workspace.document await nvim.setLine('foo') doc.forceSync() await nvim.command('normal! $') let diagnostic = Diagnostic.create(Range.create(0, 3, 1, 0), 'error', DiagnosticSeverity.Error) let collection = manager.create('empty') collection.set(doc.uri, [diagnostic]) await manager.refreshBuffer(doc.bufnr) let diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBeGreaterThanOrEqual(1) expect(diagnostics[0].message).toBe('error') collection.dispose() await manager.refreshBuffer(99) }) it('should get diagnostic next to end of line', async () => { let doc = await workspace.document await nvim.setLine('foo') doc.forceSync() await nvim.command('normal! $') let diagnostic = Diagnostic.create(Range.create(0, 3, 0, 4), 'error', DiagnosticSeverity.Error) let collection = manager.create('empty') collection.set(doc.uri, [diagnostic]) await manager.refreshBuffer(doc.bufnr) let diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBeGreaterThanOrEqual(1) expect(diagnostics[0].message).toBe('error') collection.dispose() }) it('should get diagnostic with empty range at end of line', async () => { let doc = await workspace.document await nvim.setLine('foo') doc.forceSync() await nvim.command('normal! $') let diagnostic = Diagnostic.create(Range.create(0, 3, 1, 0), 'error', DiagnosticSeverity.Error) let collection = manager.create('empty') collection.set(doc.uri, [diagnostic]) await manager.refreshBuffer(doc.bufnr) let diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBeGreaterThanOrEqual(1) expect(diagnostics[0].message).toBe('error') collection.dispose() }) it('should get diagnostic pass end of the buffer lines', async () => { let doc = await workspace.document await nvim.setLine('foo') doc.forceSync() await nvim.command('normal! ^') let diagnostic = Diagnostic.create(Range.create(1, 0, 1, 0), 'error', DiagnosticSeverity.Error) let collection = manager.create('empty') collection.set(doc.uri, [diagnostic]) await manager.refreshBuffer(doc.bufnr) let diagnostics = await manager.getCurrentDiagnostics() expect(diagnostics.length).toBeGreaterThanOrEqual(1) expect(diagnostics[0].message).toBe('error') collection.dispose() }) }) describe('jumpRelated', () => { it('should does nothing when no diagnostic exists', async () => { let doc = await workspace.document await nvim.call('cursor', [1, 1]) await commands.executeCommand('workspace.diagnosticRelated') let bufnr = await nvim.eval('bufnr("%")') expect(bufnr).toBe(doc.bufnr) }) it('should does nothing when no related information exists', async () => { let doc = await createDocument() await nvim.call('cursor', [1, 4]) await manager.jumpRelated() let bufnr = await nvim.eval('bufnr("%")') expect(bufnr).toBe(doc.bufnr) }) it('should jump to related position', async () => { let doc = await workspace.document let range = Range.create(0, 0, 0, 10) let location = Location.create(URI.file(__filename).toString(), range) let diagnostic = Diagnostic.create(range, 'msg', DiagnosticSeverity.Error, 1000, 'test', [{ location, message: 'test' }]) let collection = manager.create('positions') collection.set(doc.uri, [diagnostic]) await manager.refreshBuffer(doc.uri) await nvim.call('cursor', [1, 1]) await manager.jumpRelated() let bufname = await nvim.call('bufname', '%') expect(bufname).toMatch('diagnosticManager') }) it('should open location list', async () => { let doc = await workspace.document let range = Range.create(0, 0, 0, 10) let diagnostic = Diagnostic.create(range, 'msg', DiagnosticSeverity.Error, 1000, 'test', [{ location: Location.create(URI.file(__filename).toString(), Range.create(1, 0, 1, 10)), message: 'foo' }, { location: Location.create(URI.file(__filename).toString(), Range.create(2, 0, 2, 10)), message: 'bar' }]) let collection = manager.create('positions') collection.set(doc.uri, [diagnostic]) await manager.refreshBuffer(doc.uri) await nvim.call('cursor', [1, 1]) await manager.jumpRelated() await helper.waitFor('bufname', ['%'], 'list:///location') await nvim.input('') }) }) describe('jumpPrevious & jumpNext', () => { it('should jump to previous', async () => { let doc = await createDocument() await nvim.command('normal! G$') let ranges = manager.getSortedRanges(doc.uri, undefined) ranges.reverse() for (let i = 0; i < ranges.length; i++) { await manager.jumpPrevious() let pos = await window.getCursorPosition() expect(pos).toEqual(ranges[i].start) } await helper.doAction('diagnosticPrevious') }) it('should jump to next', async () => { let doc = await createDocument() await nvim.call('cursor', [0, 0]) let ranges = manager.getSortedRanges(doc.uri, undefined) for (let i = 0; i < ranges.length; i++) { await manager.jumpNext() let pos = await window.getCursorPosition() expect(pos).toEqual(ranges[i].start) } await helper.doAction('diagnosticNext') }) it('should consider invalid position', async () => { let doc = await helper.createDocument('foo.js') let collection = manager.create('foo') let diagnostics: Diagnostic[] = [] await doc.buffer.setLines(['foo bar', '', 'foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) await nvim.call('cursor', [2, 0]) await doc.synchronize() diagnostics.push(createDiagnostic('error', Range.create(0, 1, 0, 2), DiagnosticSeverity.Error)) diagnostics.push(createDiagnostic('warning', Range.create(1, 1, 1, 1), DiagnosticSeverity.Warning)) diagnostics.push(createDiagnostic('warning', Range.create(2, 1, 2, 1), DiagnosticSeverity.Warning)) collection.set(doc.uri, diagnostics) await manager.jumpNext() let pos = await window.getCursorPosition() expect(pos).toEqual(Position.create(2, 1)) }) it('should not throw when buffer not attached', async () => { let doc = await workspace.document await manager.jumpNext() await nvim.command('edit foo | setl buftype=nofile') doc = await workspace.document expect(doc.attached).toBe(false) await manager.jumpNext() }) it('should respect wrapscan', async () => { await createDocument() await nvim.command('setl nowrapscan') await nvim.command('normal! G$') await manager.jumpNext() let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 3, character: 2 }) await nvim.command('normal! gg0') await manager.jumpPrevious() pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 0 }) }) }) describe('diagnostic configuration', () => { it('should use filetype map from config', async () => { helper.updateConfiguration('diagnostic.filetypeMap', { default: 'bufferType' }) helper.updateConfiguration('diagnostic.messageDelay', 10) let doc = await createDocument('foo.js') await nvim.setLine('foo') await doc.synchronize() let collection = manager.getCollectionByName('test') let diagnostic = createDiagnostic('99', Range.create(0, 0, 0, 3), DiagnosticSeverity.Error) diagnostic.codeDescription = { href: 'http://www.example.com' } let diagnostics = [diagnostic] collection.set(doc.uri, diagnostics) await nvim.call('cursor', [1, 2]) await manager.echoCurrentMessage() let win = await helper.getFloat() let bufnr = await nvim.call('winbufnr', [win.id]) as number let buf = nvim.createBuffer(bufnr) let lines = await buf.lines expect(lines.join('\n')).toMatch('www.example.com') }) it('should show floating window on cursor hold', async () => { helper.updateConfiguration('diagnostic.messageTarget', 'float') helper.updateConfiguration('diagnostic.messageDelay', 10) await createDocument() await nvim.call('cursor', [1, 3]) let winid = await helper.waitFloat() let bufnr = await nvim.call('nvim_win_get_buf', winid) as number let buf = nvim.createBuffer(bufnr) let lines = await buf.lines expect(lines.join('\n')).toMatch('error') }) it('should filter diagnostics by messageLevel', async () => { helper.updateConfiguration('diagnostic.messageLevel', 'error') helper.updateConfiguration('diagnostic.messageTarget', 'echo') await createDocument() await nvim.call('cursor', [1, 6]) await manager.echoCurrentMessage() let line = await helper.getCmdline() expect(line.indexOf('warning')).toBe(-1) }) it('should echo messages on CursorHold', async () => { helper.updateConfiguration('diagnostic.messageTarget', 'echo') await createDocument() await nvim.call('cursor', [1, 3]) await helper.waitValue(async () => { let line = await helper.getCmdline() return line.length > 0 }, true) }) it('should not echo messages on CursorHold', async () => { await nvim.command('echo ""') helper.updateConfiguration('diagnostic.enableMessage', 'never') await createDocument() await nvim.call('cursor', [1, 3]) await helper.wait(30) let line = await helper.getCmdline() expect(line).toBe('') }) it('should show diagnostics of current line', async () => { helper.updateConfiguration('diagnostic.checkCurrentLine', true) await createDocument() await nvim.call('cursor', [1, 3]) let winid = await helper.waitFloat() let win = nvim.createWindow(winid) let buf = await win.buffer let lines = await buf.lines expect(lines.length).toBe(3) }) it('should filter diagnostics by level', async () => { helper.updateConfiguration('diagnostic.level', 'warning') let doc = await createDocument() let item = manager.getItem(doc.bufnr) let diagnosticsMap = manager.getDiagnostics(item) for (let diagnostics of Object.values(diagnosticsMap)) { for (let diagnostic of diagnostics) { expect(diagnostic.severity != DiagnosticSeverity.Hint).toBe(true) expect(diagnostic.severity != DiagnosticSeverity.Information).toBe(true) } } }) it('should send ale diagnostic items', async () => { helper.updateConfiguration('diagnostic.displayByAle', true) let content = ` function! MockAleResults(bufnr, collection, items) let g:collection = a:collection let g:items = a:items endfunction ` let file = await createTmpFile(content) await nvim.command(`source ${file}`) await createDocument() await helper.waitValue(async () => { let items = await nvim.getVar('items') as any return Array.isArray(items) }, true) await nvim.command('bd!') await helper.waitFor('eval', ['get(g:,"items",[])'], []) }) it('should send to vim.diagnostic', async () => { helper.updateConfiguration('diagnostic.displayByVimDiagnostic', true) let doc = await createDocument() let buf = nvim.createBuffer(doc.bufnr) let items = await buf.getVar('coc_diagnostic_map') as any expect(items.length).toBe(5) let res = await nvim.lua('return vim.diagnostic.get()') as any[] expect(res.length).toBe(5) expect(res[0].severity).toBe(1) expect(res[0].message).toBe('error') expect(res[1].source).toBe('test') }) }) describe('diagnostic util', () => { it('should get severity level', () => { expect(severityLevel('hint')).toBe(DiagnosticSeverity.Hint) expect(severityLevel('error')).toBe(DiagnosticSeverity.Error) expect(severityLevel('warning')).toBe(DiagnosticSeverity.Warning) expect(severityLevel('information')).toBe(DiagnosticSeverity.Information) expect(severityLevel('')).toBe(DiagnosticSeverity.Hint) }) it('should get Coc severity name', () => { expect(getNameFromSeverity(null as any)).toBe('CocError') expect(getNameFromSeverity(DiagnosticSeverity.Error)).toBe('CocError') expect(getNameFromSeverity(DiagnosticSeverity.Warning)).toBe('CocWarning') expect(getNameFromSeverity(DiagnosticSeverity.Information)).toBe('CocInfo') expect(getNameFromSeverity(DiagnosticSeverity.Hint)).toBe('CocHint') }) it('should get severity name', () => { expect(getSeverityName(DiagnosticSeverity.Error)).toBe('Error') expect(getSeverityName(DiagnosticSeverity.Warning)).toBe('Warning') expect(getSeverityName(DiagnosticSeverity.Information)).toBe('Information') expect(getSeverityName(DiagnosticSeverity.Hint)).toBe('Hint') }) it('should get severity type', () => { expect(getSeverityType(DiagnosticSeverity.Error)).toBe('E') expect(getSeverityType(DiagnosticSeverity.Warning)).toBe('W') expect(getSeverityType(DiagnosticSeverity.Information)).toBe('I') expect(getSeverityType(DiagnosticSeverity.Hint)).toBe('I') }) it('should sort diagnostics', () => { let diagnostics: Diagnostic[] = [ { range: Range.create(1, 0, 1, 10), message: 'a', severity: DiagnosticSeverity.Warning }, { range: Range.create(0, 0, 0, 10), message: 'b', severity: DiagnosticSeverity.Error }, { range: Range.create(0, 0, 0, 10), message: 'c', severity: DiagnosticSeverity.Error, source: 'c' }, { range: Range.create(0, 0, 0, 10), message: 'd', severity: DiagnosticSeverity.Error, source: 'd' }, ] diagnostics.sort(sortDiagnostics) expect(diagnostics.map(d => d.message)).toEqual(['c', 'd', 'b', 'a']) }) it('should get highlight group', () => { let diagnostic: Diagnostic = { range: Range.create(0, 0, 0, 10), message: 'error message', severity: DiagnosticSeverity.Error, tags: [DiagnosticTag.Deprecated, DiagnosticTag.Unnecessary] } let groups = getHighlightGroup(diagnostic) expect(groups).toContain('CocDeprecatedHighlight') expect(groups).toContain('CocUnusedHighlight') expect(groups).toContain('CocErrorHighlight') }) }) describe('toggleDiagnosticBuffer', () => { it('should not throw when bufnr is invliad or disabled', async () => { let doc = await workspace.document await helper.doAction('diagnosticToggleBuffer', 99) helper.updateConfiguration('diagnostic.enable', false) await manager.toggleDiagnosticBuffer(doc.bufnr) }) it('should toggle current buffer', async () => { let doc = await workspace.document await manager.toggleDiagnosticBuffer() let buf = nvim.createBuffer(doc.bufnr) let res = await buf.getVar('coc_diagnostic_disable') as any expect(res).toBe(1) }) it('should toggle diagnostics for buffer', async () => { let doc = await createDocument() await manager.toggleDiagnosticBuffer(doc.bufnr) let buf = nvim.createBuffer(doc.bufnr) let res = await buf.getVar('coc_diagnostic_info') as any expect(res == null).toBe(true) await manager.toggleDiagnosticBuffer(doc.bufnr, 1) res = await buf.getVar('coc_diagnostic_info') as any expect(res.error).toBe(2) }) }) describe('refresh', () => { beforeEach(() => { helper.updateConfiguration('diagnostic.autoRefresh', false) }) it('should refresh by bufnr', async () => { let doc = await createDocument() let buf = nvim.createBuffer(doc.bufnr) let res = await buf.getVar('coc_diagnostic_info') as any // should not refresh expect(res == null).toBe(true) await manager.refresh(doc.bufnr) await helper.waitValue(async () => { let res = await buf.getVar('coc_diagnostic_info') as any return res?.error }, 2) await manager.refresh(99) }) it('should refresh all buffers', async () => { let uris = ['one', 'two'].map(s => URI.file(path.join(os.tmpdir(), s)).toString()) await workspace.loadFile(uris[0], 'tabe') await workspace.loadFile(uris[1], 'tabe') let collection = manager.create('tmp') collection.set([[uris[0], [createDiagnostic('Error one')]], [uris[1], [createDiagnostic('Error two')]]]) await helper.doAction('diagnosticRefresh') let bufnrs = [workspace.getDocument(uris[0]).bufnr, workspace.getDocument(uris[1]).bufnr] for (let bufnr of bufnrs) { let buf = nvim.createBuffer(bufnr) let res = await buf.getVar('coc_diagnostic_info') as any expect(res?.error).toBe(1) } collection.dispose() }) }) }) ================================================ FILE: src/__tests__/modules/dialog.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import events from '../../events' import { Dialog, DialogButton } from '../../model/dialog' import Notification from '../../model/notification' import ProgressNotification from '../../model/progress' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('Dialog module', () => { it('should show dialog', async () => { let dialog = new Dialog(nvim, { content: '你好' }) expect(await dialog.winid).toBeNull() await dialog.show({}) let winid = await dialog.winid let win = nvim.createWindow(winid) let width = await win.width expect(width).toBe(4) await nvim.call('coc#float#close', [winid]) }) it('should invoke callback with index -1', async () => { let callback = jest.fn() let dialog = new Dialog(nvim, { content: '你好', callback, highlights: [] }) await dialog.show({}) let winid = await dialog.winid await nvim.call('coc#float#close', [winid]) await helper.wait(50) expect(callback).toHaveBeenCalledWith(-1) }) it('should invoke callback on click', async () => { let callback = jest.fn() let buttons: DialogButton[] = [{ index: 0, text: 'yes' }, { index: 1, text: 'no' }] let dialog = new Dialog(nvim, { content: '你好', buttons, callback }) await dialog.show({}) let winid = await dialog.winid let btnwin = await nvim.call('coc#float#get_related', [winid, 'buttons']) await nvim.call('win_gotoid', [btnwin]) await nvim.call('cursor', [2, 1]) await nvim.call('coc#float#nvim_float_click', []) await helper.wait(20) expect(callback).toHaveBeenCalledWith(0) }) }) describe('Notification', () => { it('should invoke callback', async () => { let n = new Notification(nvim, { content: 'foo\nbar' }) await n.show({}) await events.fire('FloatBtnClick', [n.bufnr, 1]) n.dispose() let called = false n = new Notification(nvim, { content: 'foo\nbar', buttons: [{ index: 1, text: 'text' }, { index: 2, text: 'disabled', disabled: true }], callback: () => { called = true } }) await n.show({ border: true }) await events.fire('FloatBtnClick', [n.bufnr, 0]) expect(called).toBe(true) }) }) describe('ProgressNotification', () => { it('should cancel on window close', async () => { let n = new ProgressNotification(nvim, { cancellable: true, task: (_progress, token) => { return new Promise(resolve => { token.onCancellationRequested(() => { resolve(undefined) }) }) } }) await n.show({}) let p = new Promise(resolve => { n.onDidFinish(e => { resolve(e) }) }) await nvim.call('coc#float#close_all', []) let res = await p expect(res).toBeUndefined() }) it('should not fire event when disposed', async () => { let fn = async (success: boolean) => { let n = new ProgressNotification(nvim, { cancellable: true, task: () => { return new Promise((resolve, reject) => { if (success) { setTimeout(resolve, 20) } else { setTimeout(() => { reject(new Error('timeout')) }, 20) } }) } }) let times = 0 n.onDidFinish(() => { times++ }) await n.show({}) n.dispose() await helper.wait(20) expect(times).toBe(0) } await fn(true) await fn(false) }) }) ================================================ FILE: src/__tests__/modules/document.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import path from 'path' import { Position, Range, TextEdit } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import Document, { getNotAttachReason, getUri } from '../../model/document' import { computeLinesOffsets, firstDiffLine, LinesTextDocument } from '../../model/textdocument' import { Disposable, disposeAll } from '../../util' import { applyEdits, filterSortEdits } from '../../util/textedit' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim function createTextDocument(lines: string[], eol = true): LinesTextDocument { return new LinesTextDocument('file://a', 'txt', 1, lines, 1, eol) } async function setLines(doc: Document, lines: string[]): Promise { let edit = TextEdit.insert(Position.create(0, 0), lines.join('\n')) await doc.applyEdits([edit]) } describe('LinesTextDocument', () => { it('should get first diff line ', async () => { { let res = firstDiffLine(['a', 'b'], ['a', 'b']) expect(res).toBeUndefined() } { let res = firstDiffLine(['a', 'c'], ['a', 'b']) expect(res).toEqual([2, 'c', 'b']) } { let res = firstDiffLine(['a'], ['a', 'b']) expect(res).toEqual([2, '', 'b']) } { let res = firstDiffLine(['a', 'b'], ['a']) expect(res).toEqual([2, 'b', '']) } }) it('should apply edits', () => { let textDocument = new LinesTextDocument('', '', 1, [ 'use std::io::Result;' ], 1, true) // 1234567890 let edits = [ { range: { start: { line: 0, character: 7 }, end: { line: 0, character: 11 } }, newText: "" }, { range: { start: { line: 0, character: 13 }, end: { line: 0, character: 19 } }, newText: "io" }, { range: { start: { line: 0, character: 19 }, end: { line: 0, character: 19 } }, newText: "::" }, { range: { start: { line: 0, character: 19 }, end: { line: 0, character: 19 } }, newText: "{Result, Error}" } ] edits = filterSortEdits(textDocument, edits) let res = applyEdits(textDocument, edits) expect(res).toEqual(['use std::io::{Result, Error};']) textDocument = new LinesTextDocument('', '', 1, [''], 1, true) res = applyEdits(textDocument, [TextEdit.replace(Range.create(0, 0, 1, 0), '')]) expect(res).toEqual(['']) }) it('should throw for overlapping edits', () => { let textDocument = new LinesTextDocument('', '', 1, [ 'use std::io::Result;' ], 1, true) let edits = [ { range: { start: { line: 0, character: 1 }, end: { line: 0, character: 3 } }, newText: "foo" }, { range: { start: { line: 0, character: 2 }, end: { line: 0, character: 5 } }, newText: "new" } ] expect(() => { applyEdits(textDocument, edits) }).toThrow() }) it('should return undefined when not changed', () => { let textDocument = new LinesTextDocument('', '', 1, [ 'foo bar' ], 1, true) let edits = [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, newText: "f" }, { range: { start: { line: 0, character: 2 }, end: { line: 0, character: 3 } }, newText: "o" } ] let res = applyEdits(textDocument, edits) expect(res).toBeUndefined() }) it('should get length', () => { let doc = createTextDocument(['foo']) expect(doc.length).toBe(4) expect(doc.getText().length).toBe(4) expect(doc.length).toBe(4) doc = createTextDocument(['foo'], false) expect(doc.length).toBe(3) }) it('should getText by range', () => { let doc = createTextDocument(['foo', 'bar']) expect(doc.getText(Range.create(0, 0, 0, 1))).toBe('f') expect(doc.getText(Range.create(0, 0, 1, 0))).toBe('foo\n') }) it('should get positionAt', () => { let doc = createTextDocument([], false) expect(doc.positionAt(0)).toEqual(Position.create(0, 0)) }) it('should get offsetAt', () => { let doc = createTextDocument([''], false) expect(doc.offsetAt(Position.create(1, 0))).toBe(0) expect(doc.offsetAt({ line: -1, character: -1 })).toBe(0) }) it('should work when eol enabled', () => { let doc = createTextDocument(['foo', 'bar']) expect(doc.lineCount).toBe(3) let content = doc.getText() expect(content).toBe('foo\nbar\n') content = doc.getText(Range.create(0, 0, 0, 3)) expect(content).toBe('foo') let textLine = doc.lineAt(0) expect(textLine.text).toBe('foo') textLine = doc.lineAt(Position.create(0, 3)) expect(textLine.text).toBe('foo') let pos = doc.positionAt(4) expect(pos).toEqual({ line: 1, character: 0 }) content = doc.getText(Range.create(0, 0, 0, 3)) expect(content).toBe('foo') let offset = doc.offsetAt(Position.create(0, 4)) expect(offset).toBe(4) offset = doc.offsetAt(Position.create(2, 1)) expect(offset).toBe(8) expect(doc.end).toEqual(Position.create(2, 0)) }) it('should throw for invalid line', () => { let doc = createTextDocument(['foo', 'bar']) let fn = () => { doc.lineAt(-1) } expect(fn).toThrow(Error) fn = () => { doc.lineAt(3) } expect(fn).toThrow(Error) }) it('should work when eol disabled', () => { let doc = new LinesTextDocument('file://a', 'txt', 1, ['foo'], 1, false) expect(doc.getText()).toBe('foo') expect(doc.lineCount).toBe(1) expect(doc.end).toEqual(Position.create(0, 3)) }) it('should computeLinesOffsets', () => { expect(computeLinesOffsets(['foo'], true)).toEqual([0, 4]) expect(computeLinesOffsets(['foo'], false)).toEqual([0]) }) it('should get uri for unknown buftype', () => { let res = getUri('foo', 3, '') expect(res).toBe('unknown:3') res = getUri('foo', 3, 'terminal') expect(res).toEqual('terminal:3') res = getUri(__filename, 3, 'terminal') expect(URI.parse(res).fsPath).toBe(__filename) }) it('should work with line not last one', () => { let doc = createTextDocument(['foo', 'bar']) let textLine = doc.lineAt(0) expect(textLine.lineNumber).toBe(0) expect(textLine.text).toBe('foo') expect(textLine.range).toEqual(Range.create(0, 0, 0, 3)) expect(textLine.rangeIncludingLineBreak).toEqual(Range.create(0, 0, 1, 0)) expect(textLine.isEmptyOrWhitespace).toBe(false) }) it('should work with last line', () => { let doc = createTextDocument(['foo', 'bar']) let textLine = doc.lineAt(2) expect(textLine.rangeIncludingLineBreak).toEqual(Range.create(2, 0, 2, 0)) }) it('should not attach when size exceeded', async () => { let reason = getNotAttachReason('', 1, 99) expect(reason).toMatch('exceed') }) it('should get intersect range', async () => { let doc = createTextDocument(['foo', 'bar']) let res = doc.intersectWith(Range.create(0, 0, 2, 1)) expect(res).toEqual(Range.create(0, 0, 2, 0)) }) }) describe('Document', () => { beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('properties', () => { it('should get languageId', async () => { await nvim.command(`edit +setl\\ filetype=txt.vim foo`) let doc = await workspace.document expect(doc.languageId).toBe('txt') }) it('should parse iskeyword of character range', async () => { await nvim.setOption('iskeyword', 'a-z,A-Z,48-57,_') let opt = await nvim.getOption('iskeyword') expect(opt).toBe('a-z,A-Z,48-57,_') }) it('should get start word', async () => { let doc = await workspace.document expect(doc.getStartWord('abc def')).toBe('abc') expect(doc.getStartWord('x')).toBe('x') expect(doc.getStartWord(' ')).toBe('') expect(doc.getStartWord('')).toBe('') }) it('should get word range', async () => { let doc = await workspace.document await nvim.setLine('foo bar#') await doc.synchronize() let range = doc.getWordRangeAtPosition({ line: 0, character: 0 }) expect(range).toEqual(Range.create(0, 0, 0, 3)) range = doc.getWordRangeAtPosition({ line: 0, character: 3 }) expect(range).toBeNull() range = doc.getWordRangeAtPosition({ line: 0, character: 4 }) expect(range).toEqual(Range.create(0, 4, 0, 7)) range = doc.getWordRangeAtPosition({ line: 0, character: 7 }) expect(range).toBeNull() range = doc.getWordRangeAtPosition({ line: 0, character: 7 }, '#') expect(range).toEqual(Range.create(0, 4, 0, 8)) }) it('should fix start col', async () => { let doc = await workspace.document expect(doc.fixStartcol(Position.create(0, 3), ['#'])).toBe(0) await nvim.setLine('foo #def') expect(doc.fixStartcol(Position.create(0, 6), ['#'])).toBe(4) }) it('should get lines', async () => { let doc = await workspace.document let lines = doc.getLines() expect(lines).toEqual(['']) }) it('should add additional keywords', async () => { await nvim.command(`edit foo | let b:coc_additional_keywords=['#']`) let doc = await workspace.document expect(doc.isWord('#')).toBe(true) }) it('should check has changed', async () => { let doc = await workspace.document expect(doc.hasChanged).toBe(false) await nvim.setLine('foo bar') await helper.waitValue(() => { return doc.hasChanged }, false) }) it('should get symbol ranges', async () => { let doc = await workspace.document await nvim.setLine('-foo bar foo') let ranges = doc.getSymbolRanges('foo') expect(ranges.length).toBe(2) }) it('should get current line', async () => { let doc = await workspace.document await setLines(doc, ['first line', 'second line']) let line = doc.getline(1, true) expect(line).toBe('second line') line = doc.getline(0, false) expect(line).toBe('first line') }) it('should get variable form buffer', async () => { await nvim.command('autocmd BufNewFile,BufRead * let b:coc_variable = 1') let doc = await helper.createDocument() let val = doc.getVar('variable') expect(val).toBe(1) }) it('should attach change events', async () => { let doc = await workspace.document await nvim.setLine('abc') await doc.patchChange() let content = doc.getDocumentContent() expect(content.indexOf('abc')).toBe(0) }) it('should not attach change events when b:coc_enabled is false', async () => { nvim.command('edit t|let b:coc_enabled = 0', true) let doc = await workspace.document let val = doc.getVar('enabled', 0) expect(val).toBe(0) await nvim.setLine('abc') await doc.patchChange() let content = doc.getDocumentContent() expect(content.indexOf('abc')).toBe(-1) expect(doc.notAttachReason).toMatch('coc_enabled') }) it('should attach nofile document by b:coc_force_attach', async () => { nvim.command(`e +setl\\ buftype=nofile foo| let b:coc_force_attach = 1`, true) let doc = await workspace.document expect(doc.buftype).toBe('nofile') expect(doc.attached).toBe(true) }) it('should not attach nofile buffer', async () => { nvim.command('edit t|setl buftype=nofile', true) let doc = await workspace.document expect(doc.notAttachReason).toMatch('nofile') }) it('should get lineCount, previewwindow, winid', async () => { let doc = await workspace.document let { lineCount, winid } = doc expect(lineCount).toBe(1) expect(winid != -1).toBe(true) }) }) describe('attach()', () => { it('should not attach when buffer not loaded', async () => { await nvim.command('tabe foo | doautocmd CursorHold') let doc = await workspace.document let spy = jest.spyOn(doc.buffer, 'attach').mockImplementation(() => { return Promise.reject(new Error('detached')) }) doc.attach() spy.mockRestore() await nvim.command(`bd ${doc.bufnr}`) doc.attach() await helper.wait(10) expect(doc.attached).toBe(false) await doc.synchronize() }) it('should consider eol option', async () => { await nvim.command('edit foo|setl noeol') await nvim.setLine('foo') let doc = await workspace.document expect(typeof doc.hasChanged).toBe('boolean') await doc.patchChange() await helper.waitValue(() => doc.content, 'foo') }) }) describe('applyEdits()', () => { it('should not throw with old API', async () => { let doc = await workspace.document await doc.applyEdits(nvim as any, [] as any) expect(doc.previewwindow).toBe(false) }) it('should not apply when not change happens', async () => { let doc = await workspace.document let res = await doc.applyEdits([TextEdit.insert(Position.create(0, 0), '')]) expect(res).toBeUndefined() }) it('should simple applyEdits', async () => { let doc = await workspace.document let edits: TextEdit[] = [] edits.push({ range: Range.create(0, 0, 0, 0), newText: 'a\n' }) edits.push({ range: Range.create(0, 0, 0, 0), newText: 'b\n' }) let edit = await doc.applyEdits(edits) let content = doc.getDocumentContent() expect(content).toBe('a\nb\n\n') await doc.applyEdits([edit]) expect(doc.getDocumentContent()).toEqual('\n') }) it('should return revert edit', async () => { let doc = await workspace.document let edit = await doc.applyEdits([TextEdit.replace(Range.create(0, 0, 0, 0), 'foo')]) expect(doc.getDocumentContent()).toBe('foo\n') edit = await doc.applyEdits([edit]) expect(doc.getDocumentContent()).toBe('\n') edit = await doc.applyEdits([edit]) expect(doc.getDocumentContent()).toBe('foo\n') }) it('should apply merged edits', async () => { let doc = await workspace.document await nvim.setLine('foo') await doc.patchChange() let edits: TextEdit[] = [] edits.push({ range: Range.create(0, 0, 0, 3), newText: '' }) edits.push({ range: Range.create(0, 0, 0, 0), newText: 'bar' }) let edit = await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe('bar') await doc.applyEdits([edit]) expect(doc.getDocumentContent()).toBe('foo\n') }) it('should apply textedit exceed end', async () => { let doc = await workspace.document let edits: TextEdit[] = [] edits.push({ range: Range.create(0, 0, 999999, 99999), newText: 'foo\n' }) await doc.applyEdits(edits) let content = doc.getDocumentContent() expect(content).toBe('foo\n') }) it('should move cursor', async () => { await nvim.input('ia') await helper.wait(30) let doc = await workspace.document let edits: TextEdit[] = [] edits.push({ range: Range.create(0, 0, 0, 1), newText: 'foo' }) await doc.applyEdits(edits, false, true) let cursor = await nvim.call('getcurpos') as number[] expect(cursor[1]).toBe(1) expect(cursor[2]).toBe(4) }) it('should applyEdits with range not sorted', async () => { let doc = await workspace.document await doc.buffer.setLines([ 'aa', 'bb', 'cc', 'dd' ], { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() let edits = [ { range: { start: { line: 3, character: 0 }, end: { line: 3, character: 1 } }, newText: "" }, { range: { start: { line: 0, character: 2 }, end: { line: 1, character: 0 } }, newText: "" }, ] await doc.applyEdits(edits) let lines = await doc.buffer.lines expect(lines).toEqual(['aabb', 'cc', 'd']) }) it('should applyEdits with insert as same position', async () => { let doc = await workspace.document await doc.buffer.setLines([ 'foo' ], { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() let edits = [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: 'aa' }, { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: 'bb' }, ] await doc.applyEdits(edits) let lines = await nvim.call('getline', [1, '$']) expect(lines).toEqual(['aabbfoo']) }) it('should applyEdits with bad range', async () => { let doc = await workspace.document await doc.buffer.setLines([], { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() let edits = [{ range: { start: { line: -1, character: -1 }, end: { line: -1, character: -1 } }, newText: 'foo' },] await doc.applyEdits(edits) let lines = await nvim.call('getline', [1, '$']) expect(lines).toEqual(['foo']) }) it('should applyEdits with lines', async () => { let doc = await workspace.document await doc.buffer.setLines([ 'aa', 'bb', 'cc', 'dd' ], { start: 0, end: -1, strictIndexing: false }) await doc.patchChange() let edits = [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, newText: "" }, { range: { start: { line: 0, character: 2 }, end: { line: 1, character: 0 } }, newText: "" }, ] await doc.applyEdits(edits) let lines = await nvim.call('getline', [1, '$']) expect(lines).toEqual(['abb', 'cc', 'dd']) }) it('should applyEdits with changed lines', async () => { let doc = await workspace.document let buf = doc.buffer const assertChange = async (sl, sc, el, ec, text, lines) => { let r = Range.create(sl, sc, el, ec) let edits = [TextEdit.replace(r, text)] await doc.applyEdits(edits) let curr = await buf.lines expect(curr).toEqual(lines) } await nvim.setLine('a') await doc.patchChange() await assertChange(0, 1, 0, 1, '\nb', ['a', 'b']) await assertChange(1, 0, 2, 0, 'c\n', ['a', 'c']) await assertChange(1, 0, 2, 0, '', ['a']) await assertChange(1, 0, 1, 0, 'b\nc\n', ['a', 'b', 'c']) await assertChange(2, 0, 3, 0, 'e\n', ['a', 'b', 'e']) }) it('should apply single textedit', async () => { let doc = await workspace.document let buf = doc.buffer const assertChange = async (sl, sc, el, ec, text, lines) => { let r = Range.create(sl, sc, el, ec) let edits = [TextEdit.replace(r, text)] await doc.applyEdits(edits) let curr = await buf.lines expect(curr).toEqual(lines) } await nvim.setLine('foo') await doc.patchChange() await assertChange(1, 0, 1, 0, 'bar', ['foo', 'bar']) await assertChange(2, 0, 2, 0, 'do\n', ['foo', 'bar', 'do']) await assertChange(2, 1, 3, 0, '', ['foo', 'bar', 'd']) await assertChange(2, 0, 3, 0, 'if', ['foo', 'bar', 'if']) await assertChange(2, 0, 2, 2, 'x', ['foo', 'bar', 'x']) }) it('should apply multiple edits', async () => { let arr = new Array(200) arr.fill('foo bar a b c d e') let ranges: Range[] = [] let edits: TextEdit[] = [] for (let i = 0; i < arr.length; i++) { ranges.push(Range.create(i, 0, i, 3)) ranges.push(Range.create(i, 4, i, 7)) ranges.push(Range.create(i, 8, i, 9)) ranges.push(Range.create(i, 10, i, 11)) ranges.push(Range.create(i, 12, i, 13)) ranges.push(Range.create(i, 14, i, 15)) ranges.push(Range.create(i, 16, i, 17)) edits.push(TextEdit.insert(Position.create(i, 0), `${i + 1} `)) } let doc = await helper.createDocument() let buf = doc.buffer await buf.setLines(arr) buf.highlightRanges('test', 'MoreMsg', ranges) await doc.patchChange() await doc.applyEdits(edits) }) it('should consider latest change', async () => { let doc = await helper.createDocument() let buf = doc.buffer { let edits: TextEdit[] = [TextEdit.insert(Position.create(0, 0), 'bar')] nvim.call('setline', [1, 'foo'], true) await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe('barfoo') } { await buf.setLines([' foo']) await doc.patchChange() nvim.call('setline', [1, ' fooa'], true) nvim.call('cursor', [1, 7], true) let edits: TextEdit[] = [TextEdit.del(Range.create(0, 0, 0, 1))] await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe(' fooa') } { await buf.setLines(['foo']) await nvim.call('cursor', [1, 3]) await doc.synchronize() nvim.call('setline', [1, 'fo'], true) let edits: TextEdit[] = [TextEdit.insert(Position.create(0, 0), ' ')] await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe(' fo') } }) }) describe('changeLines()', () => { it('should change lines', async () => { let doc = await workspace.document await doc.changeLines([[0, '']]) await doc.buffer.replace(['a', 'b', 'c'], 0) await doc.changeLines([[0, 'd'], [2, 'f']]) let lines = await doc.buffer.lines expect(lines).toEqual(['d', 'b', 'f']) }) }) describe('getOffset()', () => { it('should get offset', async () => { let doc = await workspace.document let offset = doc.getOffset(1, 0) expect(offset).toBe(0) }) }) describe('synchronize', () => { it('should synchronize on lines change', async () => { let document = await workspace.document let doc = TextDocument.create('untitled:1', 'txt', 1, document.getDocumentContent()) let disposables = [] document.onDocumentChange(e => { TextDocument.update(doc, e.contentChanges.slice(), 2) }, null, disposables) // document.on await nvim.setLine('abc') document.forceSync() expect(doc.getText()).toBe('abc\n') disposeAll(disposables) }) it('should synchronize changes after applyEdits', async () => { let document = await workspace.document let doc = TextDocument.create('untitled:1', 'txt', 1, document.getDocumentContent()) let disposables = [] document.onDocumentChange(e => { TextDocument.update(doc, e.contentChanges.slice(), e.textDocument.version) }, null, disposables) await nvim.setLine('abc') await document.patchChange() await document.applyEdits([TextEdit.insert({ line: 0, character: 0 }, 'd')]) expect(doc.getText()).toBe('dabc\n') disposeAll(disposables) }) it('should consider empty lines', async () => { let document = await workspace.document await nvim.call('setline', [1, ['foo', 'bar']]) await document.patchChange() await nvim.command('normal! ggdG') await nvim.call('append', [1, ['foo', 'bar']]) await document.patchChange() let lines = document.textDocument.lines expect(lines).toEqual(['', 'foo', 'bar']) }) }) describe('recreate', () => { async function assertDocument(fn: (doc: Document) => Promise): Promise { let disposables: Disposable[] = [] let fsPath = path.join(__dirname, 'document.txt') fs.writeFileSync(fsPath, '{\nfoo\n}\n', 'utf8') await helper.edit(fsPath) let document = await workspace.document document.forceSync() let doc = TextDocument.create(document.uri, 'txt', document.version, document.getDocumentContent()) let uri = doc.uri workspace.onDidOpenTextDocument(e => { if (e.uri == uri) { doc = TextDocument.create(e.uri, 'txt', e.version, e.getText()) } }, null, disposables) workspace.onDidCloseTextDocument(e => { if (e.uri == doc.uri) doc = null }, null, disposables) workspace.onDidChangeTextDocument(e => { TextDocument.update(doc, e.contentChanges.slice(), e.textDocument.version) }, null, disposables) await fn(document) document = await workspace.document document.forceSync() let text = document.getDocumentContent() expect(doc).toBeDefined() expect(doc.getText()).toBe(text) disposeAll(disposables) fs.unlinkSync(fsPath) } it('should synchronize after make changes', async () => { await assertDocument(async () => { await nvim.call('setline', [1, 'a']) await nvim.call('setline', [2, 'b']) }) }) it('should synchronize after edit', async () => { await assertDocument(async doc => { let fsPath = URI.parse(doc.uri).fsPath fs.writeFileSync(fsPath, '{\n}\n', 'utf8') await nvim.command('edit') await nvim.call('deletebufline', [doc.bufnr, 1]) doc = await workspace.document let content = doc.getDocumentContent() expect(content).toBe('}\n') }) }) it('should synchronize after force edit', async () => { await assertDocument(async doc => { let fsPath = URI.parse(doc.uri).fsPath fs.writeFileSync(fsPath, '{\n}\n', 'utf8') await nvim.command('edit') await nvim.call('deletebufline', [doc.bufnr, 1]) doc = await workspace.document let content = doc.getDocumentContent() expect(content).toBe('}\n') }) }) }) describe('applyEdits', () => { it('should synchronize on enter', async () => { let doc = await workspace.document await doc.buffer.setLines(['foox', 'bar']) await nvim.call('cursor', [1, 2]) await nvim.input('a') await doc.synchronize() void nvim.input('x') await doc.applyEdits([{ range: Range.create(0, 0, 1, 3), newText: '"foox"\n"bar"' }]) await helper.waitFor('getline', ['.'], 'xox"') let lines = await doc.buffer.lines expect(lines).toEqual(['"fo', 'xox"', '"bar"']) }) it('should synchronize content add on apply', async () => { let doc = await workspace.document await doc.buffer.setLines(['aaa', 'bbb', 'ccc']) await nvim.call('cursor', [2, 1]) void nvim.input('Ab') await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: '1' }, { range: Range.create(1, 0, 1, 0), newText: '2' }, { range: Range.create(2, 0, 2, 0), newText: '3' }, { range: Range.create(2, 3, 2, 3), newText: '\nfoo' }]) await helper.waitFor('getline', ['.'], '2bbbb') let lines = doc.getLines() expect(lines).toEqual(['1aaa', '2bbbb', '3ccc', 'foo']) }) it('should synchronize content change on multiple lines change', async () => { let arr = (new Array(40)).fill('') let doc = await workspace.document await doc.buffer.setLines(arr) await nvim.call('cursor', [1, 1]) let edits: TextEdit[] = [] let contents = [] for (let i = 0; i < arr.length; i++) { edits.push(TextEdit.insert(Position.create(i, 0), `${i}`)) contents.push(`${i}`) } void nvim.input('Ax') await doc.applyEdits(edits) await helper.waitFor('getline', ['.'], '0x') contents[0] = '0x' let lines = doc.getLines() expect(lines).toEqual(contents) }) it('should synchronize content delete', async () => { let doc = await workspace.document await doc.buffer.setLines(['foo f', 'bar']) await doc.synchronize() await nvim.command('normal! ^2l') void nvim.input('a') await doc.applyEdits([{ range: Range.create(0, 0, 1, 3), newText: 'foo foo' }]) await helper.waitFor('getline', ['.'], 'fo foo') let lines = await doc.buffer.lines expect(lines).toEqual(['fo foo']) }) }) describe('highlights', () => { it('should add highlights to document', async () => { let buf = await nvim.buffer await buf.setLines(['你好', 'world'], { start: 0, end: -1, strictIndexing: false }) let ranges = [ Range.create(0, 0, 0, 2), Range.create(1, 0, 1, 3) ] let ns = await nvim.createNamespace('coc-highlight') nvim.pauseNotification() buf.highlightRanges('highlight', 'Search', ranges) await nvim.resumeNotification() let markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBe(2) nvim.pauseNotification() buf.clearNamespace('highlight') await nvim.resumeNotification() markers = await buf.getExtMarks(ns, 0, -1) expect(markers.length).toBe(0) }) it('should add and clear highlights of current window', async () => { let buf = await nvim.buffer await buf.setLines(['你好', 'world'], { start: 0, end: -1, strictIndexing: false }) let win = await nvim.window let ranges = [ Range.create(0, 0, 0, 2), Range.create(1, 0, 1, 3) ] let res = await win.highlightRanges('Search', ranges) expect(res.length).toBe(1) let matches = await nvim.call('getmatches', [win.id]) as any nvim.pauseNotification() win.clearMatchGroup('Search') await nvim.resumeNotification() matches = await nvim.call('getmatches', [win.id]) expect(matches.length).toBe(0) }) it('should clear matches by ids', async () => { let buf = await nvim.buffer await buf.setLines(['你好', 'world'], { start: 0, end: -1, strictIndexing: false }) let win = await nvim.window let ranges = [ Range.create(0, 0, 0, 2), Range.create(1, 0, 1, 3) ] let ids = await win.highlightRanges('Search', ranges) nvim.pauseNotification() win.clearMatches(ids) await nvim.resumeNotification() let matches = await nvim.call('getmatches', [win.id]) as any expect(matches.length).toBe(0) }) }) }) ================================================ FILE: src/__tests__/modules/events.test.ts ================================================ import { CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol' import events from '../../events' import { disposeAll, wait } from '../../util' import { CancellationError } from '../../util/errors' const disposables: Disposable[] = [] afterEach(async () => { disposeAll(disposables) }) describe('register handler', () => { it('should fire InsertEnter and InsertLeave when necessary', async () => { let fn = jest.fn() events.on('InsertEnter', fn, null, disposables) events.on('InsertLeave', fn, null, disposables) expect(events.pumvisible).toBe(false) expect(events.insertMode).toBe(false) await events.fire('CursorMovedI', [1, [1, 1]]) expect(events.insertMode).toBe(false) await events.fire('CursorMoved', [1, [1, 1]]) expect(events.insertMode).toBe(false) expect(fn).toHaveBeenCalledTimes(2) }) it('should fire only once', async () => { let fn = jest.fn() events.once('ready', () => { fn() }) await events.fire('ready', []) await events.fire('ready', []) await events.fire('ready', []) expect(fn).toHaveBeenCalledTimes(1) }) it('should fire visible event once', async () => { let fn = jest.fn() let event events.once('WindowVisible', ev => { event = ev fn() }) await events.fire('BufWinEnter', [1, 1000, [1, 2]]) await events.fire('WinScrolled', [1000, 2, [2, 3]]) await wait(20) await events.fire('WinClosed', [1000]) expect(fn).toHaveBeenCalledTimes(1) expect(event).toEqual({ bufnr: 2, winid: 1000, region: [2, 3] }) }) it('should cancel visible event', async () => { let fn = jest.fn() events.once('WindowVisible', () => { fn() }) await events.fire('BufWinEnter', [1, 1000]) await events.fire('WinClosed', [1000]) await wait(10) expect(fn).toHaveBeenCalledTimes(0) }) it('should track slow handler', async () => { events.on('BufWritePre', async () => { await wait(50) }, null, disposables) events.timeout = 20 events.requesting = true await events.fire('BufWritePre', [1, '', 1]) events.requesting = false events.timeout = 1000 }) it('should on throw on handler error', async () => { events.on('BufWritePre', async () => { throw new Error('test error') }, null, disposables) events.on('BufWritePre', () => { throw new CancellationError() }, null, disposables) await events.fire('BufWritePre', [1, '', 1]) }) it('should register single handler', async () => { let fn = jest.fn() let obj = {} let disposable = events.on('BufEnter', fn, obj) disposables.push(disposable) await events.fire('BufEnter', ['a', 'b']) expect(fn).toHaveBeenCalledWith('a', 'b') }) it('should register multiple events', async () => { let fn = jest.fn() let disposable = events.on(['TaskExit', 'TaskStderr'], fn) disposables.push(disposable) await events.fire('TaskExit', []) await events.fire('TaskStderr', []) expect(fn).toHaveBeenCalledTimes(2) }) it('should resolve after timeout', async () => { let fn = (): Promise => new Promise(resolve => { setTimeout(() => { resolve() }, 20) }) let disposable = events.on('FocusGained', fn, {}) disposables.push(disposable) let ts = Date.now() await events.fire('FocusGained', []) expect(Date.now() - ts >= 10).toBe(true) }) it('should emit TextInsert after TextChangedI', async () => { let arr: string[] = [] events.on('TextInsert', () => { arr.push('insert') }, null, disposables) events.on('TextChangedI', () => { arr.push('change') }, null, disposables) await events.fire('InsertCharPre', ['i', 1]) await events.fire('TextChangedI', [1, { lnum: 1, col: 2, pre: 'i', changedtick: 1, line: 'i' }]) expect(events.lastChangeTs).toBeDefined() await events.race(['TextInsert']) expect(arr).toEqual(['change', 'insert']) await events.fire('ModeChanged', [{ old_mode: 'n', new_mode: 'i' }]) expect(events.mode).toBeDefined() }) it('should race events', async () => { let p = events.race(['InsertCharPre', 'TextChangedI', 'MenuPopupChanged']) await events.fire('InsertCharPre', ['i', 1]) await events.fire('TextChangedI', [1, { lnum: 1, col: 2, pre: 'i', changedtick: 1 }]) let res = await p expect(res.name).toBe('InsertCharPre') res = await events.race(['TextChanged'], 50) expect(res).toBeUndefined() }) it('should race same events', async () => { let arr: any[] = [] void events.race(['TextChangedI'], 200).then(res => { arr.push(res) }) void events.race(['TextChangedI'], 200).then(res => { arr.push(res) }) await events.fire('TextChangedI', [2, {}]) expect(arr.length).toBe(2) expect(arr.map(o => o.name)).toEqual(['TextChangedI', 'TextChangedI']) }) it('should cancel race by CancellationToken', async () => { let tokenSource = new CancellationTokenSource() setTimeout(() => { tokenSource.cancel() }, 20) let res = await events.race(['TextChanged'], tokenSource.token) expect(res).toBeUndefined() }) }) ================================================ FILE: src/__tests__/modules/extensionInstaller.test.ts ================================================ import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { getDependencies, getExtensionDependencies, Info, Installer, isNpmCommand, isYarn, registryUrl } from '../../extension/installer' import { remove } from '../../util/fs' const rcfile = path.join(os.tmpdir(), '.npmrc') let tmpfolder: string afterEach(() => { if (tmpfolder) { fs.rmSync(tmpfolder, { force: true, recursive: true }) } }) describe('utils', () => { it('should getDependencies & getExtensionDependencies', async () => { expect(getDependencies({})).toEqual([]) expect(getDependencies({ dependencies: { 'coc.nvim': '0.0.1' } })).toEqual([]) expect(getExtensionDependencies({})).toEqual([]) expect(getExtensionDependencies({ extensionDependencies: ['extension-1', 'extension-1'] })).toEqual(['extension-1']) }) it('should check command is npm or yarn', async () => { expect(isNpmCommand('npm')).toBe(true) expect(isYarn('yarnpkg')).toBe(true) }) it('should get registry url', async () => { const getUrl = () => { return registryUrl(os.tmpdir()) } fs.rmSync(rcfile, { force: true, recursive: true }) expect(getUrl().toString()).toBe('https://registry.npmjs.org/') fs.writeFileSync(rcfile, '', 'utf8') expect(getUrl().toString()).toBe('https://registry.npmjs.org/') fs.writeFileSync(rcfile, 'coc.nvim:registry=https://example.org', 'utf8') expect(getUrl().toString()).toBe('https://example.org/') fs.writeFileSync(rcfile, '#coc.nvim:registry=https://example.org', 'utf8') expect(getUrl().toString()).toBe('https://registry.npmjs.org/') fs.writeFileSync(rcfile, 'coc.nvim:registry=example.org', 'utf8') expect(getUrl().toString()).toBe('https://registry.npmjs.org/') fs.rmSync(rcfile, { force: true, recursive: true }) }) it('should parse name & version', async () => { const getInfo = (def: string): { name?: string, version?: string } => { let installer = new Installer(__dirname, 'npm', def) return installer.info } expect(getInfo('https://github.com')).toEqual({ name: undefined, version: undefined }) expect(getInfo('@yaegassy/coc-intelephense')).toEqual({ name: '@yaegassy/coc-intelephense', version: undefined }) expect(getInfo('@yaegassy/coc-intelephense@1.0.0')).toEqual({ name: '@yaegassy/coc-intelephense', version: '1.0.0' }) expect(getInfo('foo@1.0.0')).toEqual({ name: 'foo', version: '1.0.0' }) }) }) describe('Installer', () => { describe('fetch() & download()', () => { it('should throw with invalid url', async () => { let installer = new Installer(__dirname, 'npm', 'foo') let fn = async () => { await installer.fetch('url') } await expect(fn()).rejects.toThrow() fn = async () => { await installer.download('url', { dest: '' }) } await expect(fn()).rejects.toThrow() }) }) describe('getInfo()', () => { it('should get install arguments', async () => { let installer = new Installer(__dirname, 'npm', 'https://github.com/') expect(installer.getInstallArguments('pnpm', 'https://github.com/')).toEqual({ env: 'development', args: ['install'] }) expect(installer.getInstallArguments('npm', '')).toEqual({ env: 'production', args: ['install', '--ignore-scripts', '--no-package-lock', '--omit=dev', '--legacy-peer-deps', '--no-global'] }) expect(installer.getInstallArguments('yarn', '')).toEqual({ env: 'production', args: ['install', '--ignore-scripts', '--no-lockfile', '--production', '--ignore-engines'] }) expect(installer.getInstallArguments('pnpm', '')).toEqual({ env: 'production', args: ['install', '--ignore-scripts', '--no-lockfile', '--production', '--config.strict-peer-dependencies=false'] }) }) it('should getInfo from url', async () => { let installer = new Installer(__dirname, 'npm', 'https://github.com/') let spy = jest.spyOn(installer, 'getInfoFromUri').mockImplementation(() => { return Promise.resolve({ name: 'vue-vscode-snippets', version: '1.0.0' }) }) let res = await installer.getInfo() expect(res).toBeDefined() spy.mockRestore() }) it('should use latest version', async () => { let installer = new Installer(__dirname, 'npm', 'coc-omni') let spy = jest.spyOn(installer, 'fetch').mockImplementation(url => { expect(url.toString()).toMatch('coc-omni') return Promise.resolve(JSON.stringify({ name: 'coc-omni', 'dist-tags': { latest: '1.0.0' }, versions: { '1.0.0': { version: '1.0.0', dist: { tarball: 'tarball' }, engines: { coc: '>=0.0.80' } } } })) }) let info = await installer.getInfo() expect(info).toBeDefined() spy.mockRestore() }) it('should throw when version not found', async () => { let installer = new Installer(__dirname, 'npm', 'coc-omni@1.0.2') let spy = jest.spyOn(installer, 'fetch').mockImplementation(() => { return Promise.resolve(JSON.stringify({ name: 'coc-omni', 'dist-tags': { latest: '1.0.0' }, versions: { '1.0.0': { version: '1.0.0', dist: { tarball: 'tarball' }, engines: { coc: '>=0.0.80' } } } })) }) let fn = async () => { await installer.getInfo() } await expect(fn()).rejects.toThrow(/doesn't exists/) spy.mockRestore() }) it('should throw when not coc.nvim extension', async () => { let installer = new Installer(__dirname, 'npm', 'coc-omni') let spy = jest.spyOn(installer, 'fetch').mockImplementation(() => { return Promise.resolve(JSON.stringify({ name: 'coc-omni', 'dist-tags': { latest: '1.0.0' }, versions: { '1.0.0': { version: '1.0.0', dist: { tarball: 'tarball' } } } })) }) let fn = async () => { await installer.getInfo() } await expect(fn()).rejects.toThrow(/not a valid/) spy.mockRestore() }) }) describe('getInfoFromUri()', () => { it('should throw for url that not supported', async () => { let installer = new Installer(__dirname, 'npm', 'https://example.com') let fn = async () => { await installer.getInfoFromUri() } await expect(fn()).rejects.toThrow(/not supported/) }) it('should get info from url #1', async () => { let installer = new Installer(__dirname, 'npm', 'https://github.com/sdras/vue-vscode-snippets') let spy = jest.spyOn(installer, 'fetch').mockImplementation(() => { return Promise.resolve(JSON.stringify({ name: 'vue-vscode-snippets', version: '1.0.0' })) }) let info = await installer.getInfoFromUri() expect(info['dist.tarball']).toMatch(/master.tar.gz/) spy.mockRestore() }) it('should get info from url #2', async () => { let installer = new Installer(__dirname, 'npm', 'https://github.com/sdras/vue-vscode-snippets@main') let spy = jest.spyOn(installer, 'fetch').mockImplementation(() => { return Promise.resolve({ name: 'vue-vscode-snippets', version: '1.0.0', engines: { coc: '>=0.0.1' } }) }) let info = await installer.getInfoFromUri() expect(info['dist.tarball']).toMatch(/main.tar.gz/) expect(info['engines.coc']).toEqual('>=0.0.1') spy.mockRestore() }, 10000) }) describe('update()', () => { it('should skip install & update for symbolic folder', async () => { tmpfolder = path.join(os.tmpdir(), 'foo') fs.rmSync(tmpfolder, { recursive: true, force: true }) fs.symlinkSync(__dirname, tmpfolder, 'dir') let installer = new Installer(os.tmpdir(), 'npm', 'foo') let res = await installer.doInstall({ name: 'foo' }) expect(res).toBe(false) let val = await installer.update() expect(val).toBeUndefined() }) it('should update from url', async () => { let url = 'https://github.com/sdras/vue-vscode-snippets@main' let installer = new Installer(__dirname, 'npm', url) let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ version: '1.0.0', name: 'vue-vscode-snippets' }) }) let s = jest.spyOn(installer, 'doInstall').mockImplementation(() => { return Promise.resolve(true) }) let res = await installer.update(url) expect(res).toBeDefined() spy.mockRestore() s.mockRestore() }) it('should skip update when current version is latest', async () => { tmpfolder = path.join(os.tmpdir(), 'coc-pairs') let installer = new Installer(os.tmpdir(), 'npm', 'coc-pairs') let version = '1.0.0' let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ version }) }) let info = await installer.getInfo() fs.mkdirSync(tmpfolder) fs.writeFileSync(path.join(tmpfolder, 'package.json'), `{"version": "${info.version}"}`, 'utf8') let res = await installer.update() expect(res).toBeUndefined() spy.mockRestore() }) it('should skip update when version not satisfies', async () => { tmpfolder = path.join(os.tmpdir(), 'coc-pairs') let installer = new Installer(os.tmpdir(), 'npm', 'coc-pairs') let version = '2.0.0' let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ version, 'engines.coc': '>=99.0.0' }) }) fs.mkdirSync(tmpfolder) fs.writeFileSync(path.join(tmpfolder, 'package.json'), `{"version": "1.0.0"}`, 'utf8') let fn = async () => { await installer.update() } await expect(fn()).rejects.toThrow(Error) spy.mockRestore() }) it('should return undefined when update not performed', async () => { tmpfolder = path.join(os.tmpdir(), 'coc-pairs') let installer = new Installer(os.tmpdir(), 'npm', 'coc-pairs') let version = '2.0.0' let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ version }) }) let s = jest.spyOn(installer, 'doInstall').mockImplementation(() => { return Promise.resolve(false) }) fs.mkdirSync(tmpfolder) fs.writeFileSync(path.join(tmpfolder, 'package.json'), `{"version": "1.0.0"}`, 'utf8') let res = await installer.update() expect(res).toBeUndefined() spy.mockRestore() s.mockRestore() }) it('should update extension', async () => { tmpfolder = path.join(os.tmpdir(), 'coc-pairs') let installer = new Installer(os.tmpdir(), 'npm', 'coc-pairs') let version = '2.0.0' let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ version, name: 'coc-pairs' }) }) let s = jest.spyOn(installer, 'doInstall').mockImplementation(() => { return Promise.resolve(true) }) fs.mkdirSync(tmpfolder, { recursive: true }) fs.writeFileSync(path.join(tmpfolder, 'package.json'), `{"version": "1.0.0"}`, 'utf8') let res = await installer.update() expect(res).toBeDefined() spy.mockRestore() s.mockRestore() await remove(tmpfolder) }) }) describe('install()', () => { it('should throw when version not match required', async () => { let installer = new Installer(__dirname, 'npm', 'coc-omni') let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ name: 'coc-omni', version: '1.0.0', 'dist.tarball': '', 'engines.coc': '>=99.0.0' }) }) let fn = async () => { await installer.install() } await expect(fn()).rejects.toThrow(Error) spy.mockRestore() }) it('should return install info', async () => { let installer = new Installer(__dirname, 'npm', 'coc-omni') let spy = jest.spyOn(installer, 'getInfo').mockImplementation(() => { return Promise.resolve({ name: 'coc-omni', version: '1.0.0', 'dist.tarball': '', 'engines.coc': '>=0.0.1' }) }) let s = jest.spyOn(installer, 'doInstall').mockImplementation(() => { return Promise.resolve(true) }) let res = await installer.install() expect(res.updated).toBe(true) s.mockRestore() spy.mockRestore() }) it('should throw and remove folder when download failed', async () => { tmpfolder = path.join(os.tmpdir(), uuid()) let installer = new Installer(tmpfolder, 'npm', 'coc-omni') let folder: string let option: any let spy = jest.spyOn(installer, 'download').mockImplementation((_url, opt) => { folder = opt.dest option = opt fs.mkdirSync(folder, { recursive: true }) throw new Error('my error') }) let info: Info = { name: 'coc-omni', version: '1.0.0', 'dist.tarball': 'https://registry.npmjs.org/-/coc-omni-1.0.0.tgz' } let fn = async () => { await installer.doInstall(info) } await expect(fn()).rejects.toThrow(Error) expect(option.etagAlgorithm).toBe('md5') let exists = fs.existsSync(folder) expect(exists).toBe(false) spy.mockRestore() }) it('should revert folder when download failed', async () => { tmpfolder = path.join(os.tmpdir(), uuid()) let installer = new Installer(tmpfolder, 'npm', 'coc-omni') let f = path.join(tmpfolder, 'coc-omni') fs.mkdirSync(f, { recursive: true }) fs.writeFileSync(path.join(f, 'package.json'), '{}', 'utf8') let spy = jest.spyOn(installer, 'download').mockImplementation(() => { throw new Error('my error') }) let info: Info = { name: 'coc-omni', version: '1.0.0', 'dist.tarball': 'tarball' } let fn = async () => { await installer.doInstall(info) } await expect(fn()).rejects.toThrow(Error) spy.mockRestore() let exist = fs.existsSync(path.join(f, 'package.json')) expect(exist).toBe(true) }) it('should install new extension', async () => { tmpfolder = path.join(os.tmpdir(), uuid()) let installer = new Installer(tmpfolder, 'npm', 'coc-omni') let f = path.join(tmpfolder, 'coc-omni') let spy = jest.spyOn(installer, 'download').mockImplementation((_url, option) => { if (option.onProgress) { option.onProgress('10') } fs.mkdirSync(option.dest, { recursive: true }) let file = path.join(option.dest, 'package.json') fs.writeFileSync(file, '{version: "1.0.0"}', 'utf8') return Promise.resolve() }) let info: Info = { name: 'coc-omni', version: '1.0.0', 'dist.tarball': 'tarball' } let res = await installer.doInstall(info) spy.mockRestore() expect(res).toBe(true) let exist = fs.existsSync(path.join(f, 'package.json')) expect(exist).toBe(true) }) it('should install new version', async () => { tmpfolder = path.join(os.tmpdir(), uuid()) let installer = new Installer(tmpfolder, 'npm', 'coc-omni') let f = path.join(tmpfolder, 'coc-omni') fs.mkdirSync(f, { recursive: true }) fs.writeFileSync(path.join(f, 'package.json'), '{}', 'utf8') let spy = jest.spyOn(installer, 'download').mockImplementation((_url, option) => { if (option.onProgress) { option.onProgress('10') } fs.mkdirSync(option.dest, { recursive: true }) let file = path.join(option.dest, 'package.json') fs.writeFileSync(file, '{version: "1.0.0"}', 'utf8') return Promise.resolve() }) let info: Info = { name: 'coc-omni', version: '1.0.0', 'dist.tarball': 'tarball' } let res = await installer.doInstall(info) spy.mockRestore() expect(res).toBe(true) let exist = fs.existsSync(path.join(f, 'package.json')) expect(exist).toBe(true) }) it('should install dependencies', async () => { let npm = path.resolve(__dirname, '../npm') tmpfolder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(tmpfolder) let installer = new Installer(tmpfolder, npm, 'coc-omni') let called = false installer.on('message', () => { called = true }) await installer.installDependencies(tmpfolder, ['a', 'b']) expect(called).toBe(true) }) it('should reject on install error', async () => { let npm = path.resolve(__dirname, '../npm') tmpfolder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(tmpfolder) let installer = new Installer(tmpfolder, npm, 'coc-omni') let spy = jest.spyOn(installer, 'getInstallArguments').mockImplementation(() => { return { env: 'production', args: ['--error'] } }) let fn = async () => { await installer.installDependencies(tmpfolder, ['a', 'b']) } await expect(fn()).rejects.toThrow(Error) spy.mockRestore() }) it('should install extension dependencies', async () => { let getInfoSpy = jest.spyOn(Installer.prototype, 'getInfo').mockImplementation(async function() { // @ts-expect-error this const name = this.info.name return { name, version: '1.0.0', 'dist.tarball': `https://example.com/${name}.tgz` } }) let downloadSpy = jest.spyOn(Installer.prototype, 'download').mockImplementation(async function(url, options) { fs.mkdirSync(options.dest, { recursive: true }) let name = path.basename(url, '.tgz') let pkg = { name, version: '1.0.0', engines: { coc: '>=0.0.1' }, extensionDependencies: name === 'coc-extension-with-dependencies' ? ['coc-dependency-1', 'coc-dependency-2'] : [] } fs.writeFileSync(path.join(options.dest, 'package.json'), JSON.stringify(pkg)) }) tmpfolder = path.join(os.tmpdir(), 'coc-test') let installer = new Installer(tmpfolder, 'npm', 'coc-extension-with-dependencies@1.0.0') await installer.install() expect(fs.existsSync(path.join(tmpfolder, 'coc-extension-with-dependencies'))).toBe(true) expect(fs.existsSync(path.join(tmpfolder, 'coc-dependency-1'))).toBe(true) expect(fs.existsSync(path.join(tmpfolder, 'coc-dependency-2'))).toBe(true) getInfoSpy.mockRestore() downloadSpy.mockRestore() }, 10000) }) }) ================================================ FILE: src/__tests__/modules/extensionManager.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import events from '../../events' import { API, checkCommand, checkFileSystem, checkLanguageId, Extension, ExtensionManager, ExtensionType, getActivationEvents, getEvents, getOnCommandList, toWorkspaceContainsPatterns } from '../../extension/manager' import { ExtensionJson, ExtensionStat } from '../../extension/stat' import { disposeAll } from '../../util' import { Extensions as ExtensionsInfo, getExtensionDefinitions, IExtensionRegistry } from '../../util/extensionRegistry' import { writeJson } from '../../util/fs' import { deepIterate } from '../../util/object' import { Registry } from '../../util/registry' import workspace from '../../workspace' import helper from '../helper' let disposables: Disposable[] = [] let nvim: Neovim let tmpfolder: string beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterEach(() => { disposeAll(disposables) if (fs.existsSync(tmpfolder)) { fs.rmSync(tmpfolder, { force: true, recursive: true }) } }) afterAll(async () => { await helper.shutdown() }) function createFolder(): string { let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder, { recursive: true }) return folder } describe('utils', () => { it('should get events', () => { expect(getEvents(undefined)).toEqual([]) expect(getEvents(['a', 'b'])).toEqual(['a', 'b']) expect(getEvents(['x:y', 'x:z'])).toEqual(['x']) }) it('should get onCommand list', async () => { let res = getOnCommandList(['onCommand:a', 'onCommand', 'onCommand:b']) expect(res).toEqual(['a', 'b']) expect(getOnCommandList(undefined)).toEqual([]) }) it('should getActivationEvents', async () => { expect(getActivationEvents({} as any)).toEqual([]) expect(getActivationEvents({ activationEvents: 1 } as any)).toEqual([]) expect(getActivationEvents({ activationEvents: ['a', ''] } as any)).toEqual(['a']) expect(getActivationEvents({ activationEvents: ['a', 1] } as any)).toEqual(['a']) }) it('should checkLanguageId', () => { expect(checkLanguageId({ languageId: 'vim', filetype: 'vim' }, [])).toBe(false) expect(checkLanguageId({ languageId: 'vim', filetype: 'vim' }, ['onLanguage:java', 'onLanguage:vim'])).toBe(true) }) it('should checkCommand', async () => { expect(checkCommand('cmd', [])).toBe(false) expect(checkCommand('cmd', ['onCommand:abc'])).toBe(false) expect(checkCommand('cmd', ['onCommand:def', 'onCommand:cmd'])).toBe(true) }) it('should checkFilesystem', async () => { expect(checkFileSystem('file:///1', [])).toBe(false) expect(checkFileSystem('file:///1', ['onFileSystem:x', 'onFileSystem:file'])).toBe(true) }) it('should toWorkspaceContainsPatterns', async () => { let res = toWorkspaceContainsPatterns(['workspaceContains:', 'workspaceContains:a.js', 'workspaceContains:b.js']) expect(res).toEqual(['a.js', 'b.js']) res = toWorkspaceContainsPatterns(['workspaceContains:', 'workspaceContains:**/b.js']) expect(res).toEqual(['**/b.js']) }) }) describe('ExtensionManager', () => { function create(folder = createFolder(), activate = false): ExtensionManager { let stats = new ExtensionStat(folder) let manager = new ExtensionManager(stats, tmpfolder) disposables.push(manager) if (activate) void manager.activateExtensions() return manager } function createExtension(folder: string, packageJSON: ExtensionJson, code?: string): void { fs.mkdirSync(folder, { recursive: true }) code = code ?? `exports.activate = () => {return {folder: "${folder}"}}` let jsonfile = path.join(folder, 'package.json') fs.writeFileSync(jsonfile, JSON.stringify(packageJSON), 'utf8') let file = packageJSON.main ?? 'index.js' fs.writeFileSync(path.join(folder, file), code, 'utf8') } function createGlobalExtension(name: string, contributes?: any): string { tmpfolder = createFolder() let extFolder = path.join(tmpfolder, 'node_modules', name) createExtension(extFolder, { name, main: 'entry.js', engines: { coc: '>=0.0.1' }, contributes }) return extFolder } describe('activateExtensions()', () => { it('should registExtensions', async () => { let res = await helper.doAction('registerExtensions') expect(res).toBe(true) }) it('should throw on error', async () => { tmpfolder = createFolder() createExtension(tmpfolder, { name: 'name', engines: { coc: '>= 0.0.80' }, activationEvents: ['onLanguage:vim'], contributes: {} }) let manager = create(tmpfolder) await manager.loadExtension(tmpfolder) await manager.activateExtensions() let fn = () => { manager.tryActivateExtensions('onLanguage', () => { throw new Error('test error') }) } expect(fn).toThrow(Error) }) it('should not throw when autoActivated throws', async () => { tmpfolder = createFolder() createExtension(tmpfolder, { name: 'name', engines: { coc: '>= 0.0.80' }, activationEvents: ['*'] }) let manager = create(tmpfolder) await manager.loadExtension(tmpfolder) let extension = manager.getExtension('name').extension let spy = jest.spyOn(manager, 'checkAutoActivate' as any).mockImplementation(() => { throw new Error('test error') }) await manager.autoActivate('name', extension) spy.mockRestore() }) it('should automatically activated', async () => { let folder = createFolder() fs.writeFileSync(path.join(folder, 'base.js'), 'foo', 'utf8') workspace.workspaceFolderControl.addWorkspaceFolder(folder, false) tmpfolder = createFolder() let code = `exports.activate = (ctx) => {return {abs: ctx.asAbsolutePath('./foo')}}` createExtension(tmpfolder, { name: 'auto', engines: { coc: '>= 0.0.80' }, activationEvents: ['workspaceContains:base.js'], contributes: { rootPatterns: [ { filetype: "javascript", patterns: [ "package.json", "jsconfig.json" ] } ] } }, code) let manager = create(tmpfolder) let spy = jest.spyOn(workspace, 'checkPatterns').mockImplementation(() => { return Promise.resolve(true) }) disposables.push(Disposable.create(() => { spy.mockRestore() })) await manager.activateExtensions() await manager.loadExtension(tmpfolder) let item = manager.getExtension('auto') await helper.waitValue(() => { return item.extension.isActive }, true) expect(manager.all.length).toBe(1) expect(manager.getExtensionState('auto')).toBe('activated') expect(item.extension.exports['abs']).toBeDefined() fs.rmSync(folder, { recursive: true, force: true }) }) }) describe('activationEvents', () => { async function createExtension(manager: ExtensionManager, ...events: string[]): Promise> { let id = uuid() let isActive = false let packageJSON = { name: id, activationEvents: events } let ext = { id, packageJSON, exports: void 0, extensionPath: '', activate: async () => { isActive = true } } as any Object.defineProperty(ext, 'isActive', { get: () => isActive }) await manager.registerInternalExtension(ext, () => { isActive = false }) return ext } it('should load local extension on runtimepath change', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, true) writeJson(path.join(tmpfolder, 'package.json'), { name: 'local', engines: { coc: '>=0.0.1' }, contributes: { configuration: { properties: { 'local.enable': { type: 'boolean', default: true, description: "Enable local" } } } } }) fs.writeFileSync(path.join(tmpfolder, 'index.js'), '') let called = false workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('local.enable')) { called = true } }) await nvim.command(`set runtimepath^=${tmpfolder}`) await helper.waitValue(() => { return manager.has('local') }, true) expect(called).toBe(true) let ext = manager.getExtension('local') expect(ext.extension.isActive).toBe(true) let c = workspace.getConfiguration('local') expect(c.get('enable')).toBe(true) fs.rmSync(tmpfolder, { force: true, recursive: true }) }) it('should activate on language', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, true) let ext = await createExtension(manager, 'workspaceContains:foobar', 'onLanguage:javascript') expect(ext.isActive).toBe(false) await nvim.command('edit /tmp/a.js') await nvim.command('setf javascript') await helper.wait(50) expect(ext.isActive).toBe(true) ext = await createExtension(manager, 'onLanguage:javascript') expect(ext.isActive).toBe(true) }) it('should activate on command', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, true) let ext = await createExtension(manager, 'onCommand:test.echo') await events.fire('Command', ['test.bac']) await events.fire('Command', ['test.echo']) await helper.wait(30) expect(ext.isActive).toBe(true) }) it('should activate on workspace contains', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, true) let ext = await createExtension(manager, 'workspaceContains:package.json') await createExtension(manager, 'workspaceContains:file_not_exists') let root = path.resolve(__dirname, '../../..') await nvim.command(`edit ${path.join(root, 'file.js')}`) await helper.waitValue(() => { return ext.isActive }, true) }) it('should activate on file system', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, true) let ext = await createExtension(manager, 'onFileSystem:zip') await nvim.command('edit zip:///a') await helper.wait(30) expect(ext.isActive).toBe(true) ext = await createExtension(manager, 'onFileSystem:zip') expect(ext.isActive).toBe(true) }) }) describe('has()', () => { it('should check current extensions', async () => { let manager = create() expect(manager.has('id')).toBe(false) expect(manager.getExtension('id')).toBeUndefined() expect(manager.loadedExtensions).toEqual([]) expect(manager.all).toEqual([]) }) }) describe('activate()', () => { it('should throw when extension not registered', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) let fn = async () => { await manager.activate('name') } await expect(fn()).rejects.toThrow(Error) fn = async () => { await manager.call('name', 'fn', []) } await expect(fn()).rejects.toThrow(Error) }) it('should activate extension with dependencies', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) let depFolder = path.join(tmpfolder, 'coc-ext-dep') createExtension(depFolder, { name: 'coc-ext-dep', engines: { coc: '>=0.0.1' } }, `exports.activate = () => { return { name: 'coc-ext-dep' } }`) let mainFolder = path.join(tmpfolder, 'coc-ext-main') createExtension(mainFolder, { name: 'coc-ext-main', engines: { coc: '>=0.0.1' }, extensionDependencies: ['coc-ext-dep'] }, `exports.activate = () => { return { name: 'coc-ext-main' } }`) await manager.loadExtension(depFolder) await manager.loadExtension(mainFolder) await manager.activate('coc-ext-main') expect(manager.getExtension('coc-ext-dep').extension.isActive).toBe(true) expect(manager.getExtension('coc-ext-main').extension.isActive).toBe(true) }) it('should fail when dependency activation fails', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) let depFolder = path.join(tmpfolder, 'coc-ext-dep') createExtension(depFolder, { name: 'coc-ext-dep', engines: { coc: '>=0.0.1' } }, `exports.activate = () => { throw new Error('Dependency failed') }`) let mainFolder = path.join(tmpfolder, 'coc-ext-main') createExtension(mainFolder, { name: 'coc-ext-main', engines: { coc: '>=0.0.1' }, extensionDependencies: ['coc-ext-dep'] }, `exports.activate = () => { return { name: 'coc-ext-main' } }`) await manager.loadExtension(depFolder) await manager.loadExtension(mainFolder) let result = await manager.activate('coc-ext-main') expect(result).toBe(false) expect(manager.getExtension('coc-ext-main').extension.isActive).toBe(false) }) it('should fail on circular dependencies', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) let ext1Folder = path.join(tmpfolder, 'coc-ext1') createExtension(ext1Folder, { name: 'coc-ext1', engines: { coc: '>=0.0.1' }, extensionDependencies: ['coc-ext2'] }, `exports.activate = () => { return { name: 'coc-ext1' } }`) let ext2Folder = path.join(tmpfolder, 'coc-ext2') createExtension(ext2Folder, { name: 'coc-ext2', engines: { coc: '>=0.0.1' }, extensionDependencies: ['coc-ext1'] }, `exports.activate = () => { return { name: 'coc-ext2' } }`) await manager.loadExtension(ext1Folder) await manager.loadExtension(ext2Folder) let result = await manager.activate('coc-ext1') expect(result).toBe(false) }) }) describe('call()', () => { it('should activate extension that not activated', async () => { tmpfolder = createFolder() let code = `exports.activate = () => {return {getId: () => {return 'foo'}}}` createExtension(tmpfolder, { name: 'name', engines: { coc: '>=0.0.1' } }, code) let manager = create(tmpfolder) await manager.loadExtension(tmpfolder) let item = manager.getExtension('name') expect(item.extension.isActive).toBe(false) let res = await manager.call('name', 'getId', []) expect(res).toBe('foo') let fn = async () => { await manager.call('name', 'fn', []) } await expect(fn()).rejects.toThrow(Error) }) }) describe('loadExtensionFile()', () => { it('should load single file extension', async () => { tmpfolder = createFolder() let filepath = path.join(tmpfolder, 'abc.js') fs.writeFileSync(filepath, `exports.activate = (ctx) => {return {storagePath: ctx.storagePath}}`, 'utf8') let manager = create(tmpfolder, true) await manager.loadExtensionFile(filepath) let item = manager.getExtension('single-abc') expect(item.extension.isActive).toBe(true) let file = path.join(tmpfolder, 'single-abc-data') expect(item.extension.exports['storagePath']).toBe(file) }) it('should not load extension when filepath not exists', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, true) let filepath = path.join(tmpfolder, 'abc.js') await manager.loadExtensionFile(filepath) let item = manager.getExtension('single-abc') expect(item).toBeUndefined() }) }) describe('uninstallExtensions()', () => { it('should show message for extensions not found', async () => { let manager = create(tmpfolder) await manager.uninstallExtensions(['foo']) let line = await helper.getCmdline() expect(line).toMatch('not found') }) }) describe('cleanExtensions()', () => { it('should return extension ids that not disabled', async () => { tmpfolder = createFolder() let foo = path.join(tmpfolder, 'foo') createExtension(foo, { name: 'foo', engines: { coc: '>=0.0.1' } }) let bar = path.join(tmpfolder, 'bar') createExtension(bar, { name: 'bar', engines: { coc: '>=0.0.1' } }) let obj = { dependencies: { foo: '1.0.0', bar: '1.0.0' } } writeJson(path.join(tmpfolder, 'package.json'), obj) let manager = create(tmpfolder) await manager.loadExtension(foo) await manager.loadExtension(bar) manager.states.setDisable('foo', true) let res = await manager.cleanExtensions() expect(res).toEqual(['bar']) }) }) describe('loadedExtension()', () => { it('should throw on bad extension', async () => { tmpfolder = createFolder() createExtension(tmpfolder, { name: 'name', engines: {} }) let manager = create(tmpfolder) let fn = async () => { await manager.loadExtension(tmpfolder) } await expect(fn()).rejects.toThrow(Error) fn = async () => { await manager.loadExtension([tmpfolder]) } await expect(fn()).rejects.toThrow(Error) }) it('should return false when disabled', async () => { tmpfolder = createFolder() createExtension(tmpfolder, { name: 'name', engines: { coc: '>=0.0.1' } }) let manager = create(tmpfolder) manager.states.setDisable('name', true) let res = await manager.loadExtension(tmpfolder) expect(res).toBe(false) }) it('should load local extension', async () => { tmpfolder = createFolder() createExtension(tmpfolder, { name: 'name', engines: { vscode: '1.0' } }) let manager = create(tmpfolder) await manager.loadExtension(tmpfolder) await manager.loadExtension([tmpfolder]) let item = manager.getExtension('name') expect(item.isLocal).toBe(true) expect(item.extension.isActive).toBe(false) await item.extension.activate() expect(item.extension.isActive).toBe(true) }) it('should load and activate global extension', async () => { let contributes = { configuration: { properties: { 'name.enable': { type: 'boolean', description: "Enable name" } } } } let extFolder = createGlobalExtension('name', contributes) let manager = create(tmpfolder) manager.states.addExtension('name', '>=0.0.1') let res = await manager.loadExtension(extFolder) await manager.activateExtensions() expect(res).toBe(true) let item = manager.getExtension('name') expect(item.isLocal).toBe(false) expect(item.extension.extensionPath.endsWith('name')).toBe(true) let result = await item.extension.activate() expect(result).toBeDefined() expect(result).toEqual(item.extension.exports) await manager.deactivate('name') let stat = manager.getExtensionState('name') expect(stat).toBe('loaded') let c = workspace.getConfiguration('name') expect(c.get('enable')).toBe(false) manager.unregistContribution('name') c = workspace.getConfiguration('name') expect(c.get('enable', undefined)).toBe(undefined) }) }) describe('unloadExtension()', () => { it('should unload extension', async () => { let extFolder = createGlobalExtension('name') let manager = create(tmpfolder) manager.states.addExtension('name', '>=0.0.1') await manager.loadExtension(extFolder) let res = manager.getExtension('name') expect(res).toBeDefined() let fn = jest.fn() manager.onDidUnloadExtension(() => { fn() }) await manager.unloadExtension('name') res = manager.getExtension('name') expect(res).toBeUndefined() await manager.unloadExtension('name') expect(fn).toHaveBeenCalledTimes(1) }) }) describe('reloadExtension()', () => { it('should throw when extension not registered', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) let fn = async () => { await manager.reloadExtension('id') } await expect(fn()).rejects.toThrow(Error) }) it('should reload single file extension', async () => { tmpfolder = createFolder() let filepath = path.join(tmpfolder, 'test.js') fs.writeFileSync(filepath, `exports.activate = () => {return {file: "${filepath}"}};exports.deactivate = () => {}`, 'utf8') let manager = create(tmpfolder) await manager.activateExtensions() await manager.loadExtensionFile(filepath) let item = manager.getExtension('single-test') expect(item.extension.isActive).toBe(true) await manager.activate('single-test') await manager.reloadExtension('single-test') item = manager.getExtension('single-test') expect(item.extension.isActive).toBe(true) await item.deactivate() expect(item.extension.isActive).toBe(false) process.env.COC_NO_PLUGINS = '1' await manager.activateExtensions() }) it('should reload extension from directory', async () => { tmpfolder = createFolder() let extFolder = path.join(tmpfolder, 'node_modules', 'name') createExtension(extFolder, { name: 'name', main: 'entry.js', engines: { coc: '>=0.0.1' } }) let manager = create(tmpfolder) let res = await manager.loadExtension(extFolder) expect(res).toBe(true) await manager.reloadExtension('name') let item = manager.getExtension('name') expect(item.extension.isActive).toBe(false) }) }) describe('registerExtension()', () => { it('should not register disabled extension', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) manager.states.setDisable('name', true) await manager.registerExtension(tmpfolder, { name: 'name', engines: { coc: '>=0.0.1' }, }, ExtensionType.Internal) let item = manager.getExtension('name') expect(item).toBeUndefined() }) it('should throw error on activate', async () => { tmpfolder = createFolder() let code = `exports.activate = () => {throw new Error('my error')}` createExtension(tmpfolder, { name: 'name', engines: { coc: '>=0.0.1' } }, code) let manager = create(tmpfolder) await manager.loadExtension(tmpfolder) let item = manager.getExtension('name') let fn = async () => { await item.extension.activate() } await expect(fn()).rejects.toThrow() fn = async () => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions item.extension.exports } await expect(fn()).rejects.toThrow() }) it('should catch error on deactivate', async () => { tmpfolder = createFolder() let code = `exports.activate = () => { return {}};exports.deactivate = () => {throw new Error('my error')}` createExtension(tmpfolder, { name: 'name', engines: { coc: '>=0.0.1' } }, code) let manager = create(tmpfolder) await manager.loadExtension(tmpfolder) let item = manager.getExtension('name') await item.deactivate() await item.extension.activate() await item.deactivate() }) it('should not throw on register error', async () => { let manager = create() let spy = jest.spyOn(manager, 'registerExtension').mockImplementation(() => { throw new Error('my error') }) manager.registerExtensions([{ root: __filename, isLocal: false, packageJSON: {} as any }]) spy.mockRestore() }) }) describe('toggleExtension()', () => { it('should not toggle disabled extension', async () => { tmpfolder = createFolder() let manager = create(tmpfolder) manager.states.setDisable('foo', true) await manager.toggleExtension('foo') }) it('should toggle single file extension', async () => { tmpfolder = createFolder() let filepath = path.join(tmpfolder, 'test.js') fs.writeFileSync(filepath, `exports.activate = () => {return {file: "${filepath}"}};exports.deactivate = () => {}`, 'utf8') let manager = create(tmpfolder, true) await manager.loadExtensionFile(filepath) await manager.toggleExtension('single-test') let item = manager.getExtension('single-test') expect(item).toBeUndefined() await manager.toggleExtension('single-test') }) it('should toggle global extension', async () => { tmpfolder = createFolder() let folder = createGlobalExtension('global') let manager = create(tmpfolder, true) manager.states.addExtension('global', '>=0.0.1') await manager.loadExtension(folder) let item = manager.getExtension('global') expect(item.extension.isActive).toBe(true) await manager.toggleExtension('global') item = manager.getExtension('global') expect(item).toBeUndefined() await manager.toggleExtension('global') item = manager.getExtension('global') expect(item.extension.isActive).toBe(true) }) it('should toggle local extension', async () => { tmpfolder = createFolder() let folder = path.join(tmpfolder, 'local') createExtension(folder, { name: 'local', main: 'entry.js', engines: { coc: '>=0.0.1' } }) let manager = create(tmpfolder, true) await manager.loadExtension(folder) let item = manager.getExtension('local') expect(item.extension.isActive).toBe(true) expect(item.isLocal).toBe(true) await manager.toggleExtension('local') item = manager.getExtension('local') expect(item).toBeUndefined() await manager.toggleExtension('local') let state = manager.getExtensionState('local') expect(state).toBe('activated') }) }) describe('watchExtension()', () => { it('should throw when watchman not found', async () => { tmpfolder = createFolder() let extFolder = path.join(tmpfolder, 'node_modules', 'name') createExtension(extFolder, { name: 'name', main: 'entry.js', engines: { coc: '>=0.0.1' } }) let manager = create(tmpfolder) let res = await manager.loadExtension(extFolder) expect(res).toBe(true) let spy = jest.spyOn(workspace.fileSystemWatchers, 'getWatchmanPath').mockImplementation(() => { return Promise.reject(new Error('not found')) }) let fn = async () => { await manager.watchExtension('name') } await expect(fn()).rejects.toThrow(Error) spy.mockRestore() await expect(async () => { await helper.doAction('watchExtension', 'not_exists_extension') }).rejects.toThrow(/not found/) }) it('should reload extension on file change', async () => { tmpfolder = createFolder() workspace.fileSystemWatchers.disabled = false let extFolder = path.join(tmpfolder, 'node_modules', 'name') createExtension(extFolder, { name: 'name', main: 'entry.js', engines: { coc: '>=0.0.1' } }) let manager = create(tmpfolder) let res = await manager.loadExtension(extFolder) expect(res).toBe(true) let called = false let fn = jest.fn() let r = jest.spyOn(workspace, 'getWatchmanPath').mockImplementation(() => { return 'watchman' }) let s = jest.spyOn(manager, 'reloadExtension').mockImplementation(() => { fn() return Promise.resolve() }) let spy = jest.spyOn(workspace.fileSystemWatchers, 'createClient').mockImplementation(() => { return { dispose: () => {}, subscribe: (_key: string, cb: Function) => { setTimeout(() => { called = true cb() }, 20) } } as any }) await manager.watchExtension('name') await helper.waitValue(() => { return called }, true) expect(fn).toHaveBeenCalled() r.mockRestore() spy.mockRestore() s.mockRestore() }) it('should watch single file extension', async () => { let dir = createFolder() let id = uuid() let filepath = path.join(dir, `${id}.js`) fs.writeFileSync(filepath, `exports.activate = () => {return {file: "${filepath}"}};exports.deactivate = () => {}`, 'utf8') let manager = create(dir) await manager.loadExtensionFile(filepath) await manager.watchExtension(`single-${id}`) let fn = async () => { await manager.watchExtension('single-unknown') } await expect(fn()).rejects.toThrow(Error) let called = false let spy = jest.spyOn(manager, 'loadExtensionFile').mockImplementation(() => { called = true return Promise.resolve('') }) await helper.waitValue(() => { return called }, true) spy.mockRestore() fs.unlinkSync(filepath) }) }) describe('loadFileExtensions', () => { it('should load extension files', async () => { tmpfolder = createFolder() let filepath = path.join(tmpfolder, 'abc.js') fs.writeFileSync(filepath, `exports.activate = (ctx) => {return {storagePath: ctx.storagePath}}`, 'utf8') let manager = create(tmpfolder, true) Object.assign(manager, { singleExtensionsRoot: tmpfolder }) await manager.loadFileExtensions() let item = manager.getExtension('single-abc') expect(item.extension.isActive).toBe(true) }) }) describe('registContribution', () => { it('should register definitions', async () => { let json = `{ "configuration": { "definitions": { "flexible": { "type": "object", "$ref": 3, "properties": { "grow": { "$ref": "#/definitions/flexible.position" }, "omit": { "$ref": "#/definitions/flexible.position" } } } }, "properties": { "explorer.presets": { "toggle": { "$ref": "#/properties/explorer.toggle" }, "mykey": { "$ref": "#/definitions/mapping.keyMappings" } } } } }` let obj = JSON.parse(json) tmpfolder = createFolder() let manager = create(tmpfolder, false) let packageJSON = { contributes: obj } manager.registContribution('@explorer', packageJSON, __dirname) const extensionRegistry = Registry.as(ExtensionsInfo.ExtensionContribution) let info = extensionRegistry.getExtension('@explorer') let definitions = info.definitions expect(definitions['explorer.flexible']).toBeDefined() let refs: string[] = [] deepIterate(definitions, (node, key) => { if (key == '$ref' && typeof node[key] === 'string') { refs.push(node[key]) } }) expect(refs).toEqual([ '#/definitions/explorer.flexible.position', '#/definitions/explorer.flexible.position' ]) refs = [] let properties = manager.configurationNodes[0].properties deepIterate(properties, (node, key) => { if (key == '$ref' && typeof node[key] === 'string') { refs.push(node[key]) } }) expect(refs).toEqual([ '#/properties/explorer.toggle', '#/definitions/explorer.mapping.keyMappings' ]) let defs = getExtensionDefinitions() expect(defs['explorer.flexible']).toBeDefined() }) }) describe('loadFileOrFolder()', () => { it('should throw for invalid extension', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, false) await expect(async () => { await manager.load('file_not_exists', false) }).rejects.toThrow(Error) let id = uuid() let filpath = path.join(os.tmpdir(), id) fs.writeFileSync(filpath, '', 'utf8') await manager.toggleExtension(`single-${id}`) await expect(async () => { await manager.load(filpath, false) }).rejects.toThrow(/disabled/) fs.rmSync(filpath, { force: true }) }) it('should load extension without active', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, false) createExtension(tmpfolder, { name: 'name', engines: { coc: '>= 0.0.80' }, activationEvents: ['*'], contributes: {} }) let res = await manager.load(tmpfolder, false) expect(res.isActive).toBe(false) expect(res.name).toBe('name') expect(res.exports).toEqual({}) await manager.activateExtensions() await res.unload() fs.rmSync(tmpfolder, { recursive: true }) }) it('should load and active extension', async () => { tmpfolder = createFolder() let manager = create(tmpfolder, false) createExtension(tmpfolder, { name: 'active', engines: { coc: '>= 0.0.80' }, activationEvents: ['*'], contributes: {} }, `exports.activate = () => 'api';exports.foo = 'bar';`) let res = await manager.load(tmpfolder, true) expect(res.isActive).toBe(true) expect(res.name).toBe('active') expect(res.api).toBe('api') expect(res.exports).toEqual({ foo: 'bar' }) await res.unload() fs.rmSync(tmpfolder, { recursive: true }) }) }) }) ================================================ FILE: src/__tests__/modules/extensionModules.test.ts ================================================ process.env.COC_NO_PLUGINS = '1' import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import events from '../../events' import { checkExtensionRoot, ExtensionStat, getExtensionName, getJsFiles, loadExtensionJson, loadGlobalJsonAsync, toInterval, validExtensionFolder } from '../../extension/stat' import { InstallBuffer, InstallChannel } from '../../extension/ui' import { disposeAll } from '../../util' import { loadJson, writeJson } from '../../util/fs' import window from '../../window' import helper from '../helper' let disposables: Disposable[] = [] let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterEach(() => { disposeAll(disposables) }) afterAll(async () => { await helper.shutdown() }) function createFolder(): string { let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder, { recursive: true }) disposables.push(Disposable.create(() => { fs.rmSync(folder, { recursive: true, force: true }) })) return folder } describe('utils', () => { describe('getJsFiles', () => { it('should get js files', async () => { let res = await getJsFiles(__dirname) expect(Array.isArray(res)).toBe(true) }) }) describe('loadGlobalJsonAsync()', () => { it('should throw when engines not valid', async () => { let folder = createFolder() let file = path.join(folder, 'package.json') fs.writeFileSync(file, '{}', 'utf8') await expect(async () => { await loadGlobalJsonAsync(folder, '0.0.80') }).rejects.toThrow(/Invalid engines/) fs.writeFileSync(file, '{"engines": {}}', 'utf8') await expect(async () => { await loadGlobalJsonAsync(folder, '0.0.80') }).rejects.toThrow(/Invalid engines/) }) it('should throw when version not match', async () => { let folder = createFolder() let file = path.join(folder, 'package.json') fs.writeFileSync(file, '{"engines": {"coc": ">=0.0.80"}}', 'utf8') await expect(async () => { await loadGlobalJsonAsync(folder, '0.0.79') }).rejects.toThrow(/not match/) }) it('should throw when main file not found', async () => { let folder = createFolder() let file = path.join(folder, 'package.json') fs.writeFileSync(file, '{"engines": {"coc": ">=0.0.80"}}', 'utf8') await expect(async () => { await loadGlobalJsonAsync(folder, '0.0.80') }).rejects.toThrow(/not found/) }) it('should load json', async () => { let folder = createFolder() let file = path.join(folder, 'package.json') fs.writeFileSync(file, '{"name": "foo","engines": {"coc": ">=0.0.80"}}', 'utf8') fs.writeFileSync(path.join(folder, 'index.js'), '', 'utf8') let res = await loadGlobalJsonAsync(folder, '0.0.80') expect(res.name).toBe('foo') }) }) describe('validExtensionFolder()', () => { it('should check validExtensionFolder', async () => { expect(validExtensionFolder(__dirname, '')).toBe(false) let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder) disposables.push(Disposable.create(() => { fs.rmSync(folder, { recursive: true, force: true }) })) writeJson(path.join(folder, 'index.js'), '') let filepath = path.join(folder, 'package.json') writeJson(filepath, { name: 'name', engines: { coc: '>=0.0.81' } }) expect(validExtensionFolder(folder, '0.0.82')).toBe(true) }) }) describe('checkExtensionRoot', () => { it('should not throw on error', async () => { let spy = jest.spyOn(fs, 'existsSync').mockImplementation(() => { throw new Error('my error') }) let called = false let s = jest.spyOn(console, 'error').mockImplementation(() => { called = true }) let root = path.join(os.tmpdir(), 'foo-bar') let res = checkExtensionRoot(root) s.mockRestore() spy.mockRestore() expect(res).toBe(false) }) it('should create root when it does not exist', async () => { let root = path.join(os.tmpdir(), 'foo-bar') let res = checkExtensionRoot(root) expect(res).toBe(true) expect(fs.existsSync(path.join(root, 'package.json'))).toBe(true) let method = typeof fs['rmSync'] === 'function' ? 'rmSync' : 'rmdirSync' fs[method](root, { recursive: true }) }) it('should remove unexpted file', async () => { let root = path.join(os.tmpdir(), uuid()) fs.writeFileSync(root, '') let res = checkExtensionRoot(root) expect(res).toBe(true) expect(fs.existsSync(path.join(root, 'package.json'))).toBe(true) let method = typeof fs['rmSync'] === 'function' ? 'rmSync' : 'rmdirSync' fs[method](root, { recursive: true }) }) }) describe('loadExtensionJson()', () => { function testErrors(data: any, version: string, count, createJs = false): any { let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder) disposables.push(Disposable.create(() => { fs.rmSync(folder, { recursive: true, force: true }) })) if (createJs) writeJson(path.join(folder, 'index.js'), '') let filepath = path.join(folder, 'package.json') if (data) writeJson(filepath, data) let errors: string[] = [] let json = loadExtensionJson(folder, version, errors) expect(errors.length).toBe(count) return json } it('should add errors', async () => { testErrors(undefined, '', 1) testErrors({}, '', 2) testErrors({ name: 'name', main: 'main' }, '', 1) testErrors({ name: 'name', engines: {} }, '', 2) testErrors({ name: 'name', engines: { coc: '>=0.0.81' } }, '0.0.79', 1, true) testErrors({ name: 'name', engines: { coc: '>=0.0.81', main: 'index.js' } }, '0.0.82', 0, true) }) it('should not check entry for vscode extension', async () => { testErrors({ name: 'name', engines: { vscode: '0.10.x' } }, '', 0) }) }) describe('getExtensionName', () => { it('should get extension name', async () => { expect(getExtensionName('foo')).toBe('foo') expect(getExtensionName('http://1')).toBe('http://1') expect(getExtensionName('@a/b')).toBe('@a/b') expect(getExtensionName('semver@1.2.3')).toBe('semver') }) }) }) describe('ExtensionStat', () => { function createDB(folder: string, data: any): string { let s = JSON.stringify(data, null, 2) let filepath = path.join(folder, 'db.json') fs.writeFileSync(filepath, s, 'utf8') return filepath } function create(): [ExtensionStat, string] { let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder) disposables.push(Disposable.create(() => { fs.rmSync(folder, { force: true, recursive: true }) })) return [new ExtensionStat(folder), path.join(folder, 'package.json')] } it('should not throw on create', async () => { let spy = jest.spyOn(ExtensionStat.prototype, 'migrate' as any).mockImplementation(() => { throw new Error('my error') }) let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(folder) let stat = new ExtensionStat(folder) spy.mockRestore() expect(stat).toBeDefined() }) it('should add local extension', async () => { let folder = path.join(os.tmpdir(), uuid()) let stat = new ExtensionStat(folder) stat.addLocalExtension('name', folder) expect(stat.getFolder('name')).toBe(folder) expect(stat.getFolder('unknown')).toBeUndefined() }) it('should addNoPromptFolder', async () => { let [state, filepath] = create() let uri = URI.file(path.dirname(filepath)).toString() expect(state.shouldPrompt(uri)).toBe(true) state.addNoPromptFolder(uri) state.addNoPromptFolder(uri) expect(state.shouldPrompt(uri)).toBe(false) }) it('should iterate activated extensions', () => { let folder = createFolder() writeJson(path.join(folder, 'package.json'), { disabled: ['x', 'y'], dependencies: { x: '', y: '', z: '', a: '' } }) let names: string[] = [] let stat = new ExtensionStat(folder) for (let name of stat.activated()) { names.push(name) } expect(names).toEqual(['z', 'a']) }) it('should migrate #1', async () => { let folder = createFolder() let stat = new ExtensionStat(folder) expect(stat.getExtensionsStat()).toEqual({}) let data = { extension: { x: { disabled: true }, y: { locked: true }, z: {} } } let filepath = createDB(folder, data) writeJson(path.join(folder, 'package.json'), { dependencies: { x: '', y: '', z: '', a: '' } }) stat = new ExtensionStat(folder) let res = stat.getExtensionsStat() expect(res).toEqual({ x: 1, y: 2, z: 0, a: 0 }) let obj = loadJson(path.join(folder, 'package.json')) as any expect(obj.disabled).toEqual(['x']) expect(obj.locked).toEqual(['y']) expect(fs.existsSync(filepath)).toBe(false) }) it('should migrate #2', async () => { let folder = createFolder() let stat = new ExtensionStat(folder) expect(stat.getExtensionsStat()).toEqual({}) let data = {} createDB(folder, data) writeJson(path.join(folder, 'package.json'), {}) stat = new ExtensionStat(folder) let res = stat.getExtensionsStat() expect(res).toEqual({}) let obj = loadJson(path.join(folder, 'package.json')) as any expect(obj.disabled).toEqual([]) expect(obj.locked).toEqual([]) }) it('should load disabled & locked from package.json', async () => { let folder = createFolder() let obj = { disabled: ['foo'], locked: ['bar'], dependencies: { foo: '', bar: '', z: '' } } writeJson(path.join(folder, 'package.json'), obj) let stat = new ExtensionStat(folder) expect(stat.disabledExtensions).toEqual(['foo']) expect(stat.lockedExtensions).toEqual(['bar']) expect(stat.getExtensionsStat()['z']).toBe(0) }) it('should add & remove extension', async () => { let [stat, jsonFile] = create() stat.addExtension('foo', '') expect(stat.getExtensionsStat()).toEqual({ foo: 0 }) let res = loadJson(jsonFile) as any expect(res).toEqual({ dependencies: { foo: '' } }) stat.removeExtension('foo',) expect(stat.isDisabled('foo')).toBe(false) expect(stat.getExtensionsStat()).toEqual({}) res = loadJson(jsonFile) as any expect(res).toEqual({ dependencies: {} }) }) it('should remove extension not exists', async () => { let [stat] = create() stat.removeExtension('foo') }) it('should remove from disabled and locked extensions', async () => { let [stat, jsonFile] = create() stat.addExtension('foo', '') stat.setDisable('foo', true) stat.setLocked('foo', true) let res = loadJson(jsonFile) as any expect(res.disabled).toEqual(['foo']) expect(res.locked).toEqual(['foo']) stat.removeExtension('foo') res = loadJson(jsonFile) as any expect(res.disabled).toEqual([]) expect(res.locked).toEqual([]) }) it('should setDisable', async () => { let [stat] = create() stat.addExtension('foo', '') stat.setDisable('foo', true) expect(stat.hasExtension('foo')).toBe(true) expect(stat.isDisabled('foo')).toBe(true) stat.setDisable('foo', false) expect(stat.isDisabled('foo')).toBe(false) expect(stat.disabledExtensions).toEqual([]) }) it('should setLocked', async () => { let [stat] = create() stat.addExtension('foo', '') stat.setLocked('foo', true) expect(stat.lockedExtensions).toEqual(['foo']) stat.setLocked('foo', false) expect(stat.lockedExtensions).toEqual([]) }) it('should check update', async () => { let [stat] = create() expect(stat.shouldUpdate('never')).toBe(false) expect(stat.shouldUpdate('daily')).toBe(true) stat.setLastUpdate() expect(stat.shouldUpdate('weekly')).toBe(false) }) it('should toInterval', async () => { expect(typeof toInterval('daily')).toBe('number') expect(typeof toInterval('weekly')).toBe('number') }) it('should get dependencies', async () => { let [stat] = create() expect(stat.dependencies).toEqual({}) expect(stat.globalIds).toEqual([]) stat.addExtension('foo', '') expect(stat.dependencies).toEqual({ foo: '' }) expect(stat.globalIds).toEqual(['foo']) }) it('should filterGlobalExtensions', async () => { let [stat, jsonFile] = create() expect(stat.filterGlobalExtensions(['foo', 'bar', undefined, 3] as any)).toEqual(['foo', 'bar']) stat.addExtension('foo', '') expect(stat.filterGlobalExtensions(['foo', 'bar'])).toEqual(['bar']) stat.setDisable('bar', true) expect(stat.filterGlobalExtensions(['foo', 'bar'])).toEqual([]) let folder = path.resolve(jsonFile, '../node_modules') fs.mkdirSync(folder) fs.mkdirSync(path.join(folder, 'uri')) writeJson(path.join(folder, 'uri', 'package.json'), {}) stat.addExtension('uri', 'http://git') stat.addExtension('simple', '') fs.mkdirSync(path.join(folder, 'simple')) writeJson(path.join(folder, 'simple', 'package.json'), {}) let res = stat.filterGlobalExtensions(['http://git']) expect(res).toEqual([]) }) }) describe('InstallBuffer', () => { afterEach(() => { events.requesting = false }) it('should sync by not split', async () => { global.__TEST__ = false let buf = new InstallBuffer({ isUpdate: false, updateUIInTab: false }) disposables.push(buf) events.requesting = true await buf.start(['a', 'b', 'c']) let wins = await nvim.windows expect(wins.length).toBe(1) global.__TEST__ = true }) it('should draw buffer with stats', async () => { let buf = new InstallBuffer({ isUpdate: true, updateUIInTab: true }) disposables.push(buf) buf.draw() await buf.start(['a', 'b', 'c', 'd']) buf.startProgress('a') buf.startProgress('b') buf.startProgress('c') buf.addMessage('a', 'Updated to 1.0.0') buf.addMessage('b', 'message') buf.finishProgress('a', true) buf.finishProgress('b', false) buf.draw() buf.finishProgress('c', true) buf.finishProgress('d', true) let buffer = await nvim.buffer let lines = await buffer.lines expect(lines.length).toBe(6) buf.draw() }) it('should stop when all items finished', async () => { let buf = new InstallBuffer({ isUpdate: false }) disposables.push(buf) await buf.start(['a', 'b']) buf.startProgress('a') buf.startProgress('b') expect(buf.remains).toBe(2) buf.finishProgress('a', true) buf.finishProgress('b', true) buf.draw() expect(buf.getMessages(0)).toEqual([]) expect(buf.stopped).toBe(true) }) it('should show messages and dispose', async () => { events.requesting = true let buf = new InstallBuffer({ isUpdate: true }) disposables.push(buf) await buf.start(['a', 'b']) buf.startProgress('a') buf.addMessage('a', 'start') buf.addMessage('a', 'finish') buf.finishProgress('a', true) buf.draw() let bufnr = await nvim.call('bufnr', ['%']) await nvim.call('cursor', [3, 4]) let id = await helper.waitFloat() let win = nvim.createWindow(id) let buffer = await win.buffer let lines = await buffer.lines expect(lines.join(' ')).toBe('start finish') await nvim.command(`bd! ${bufnr}`) expect(buf.stopped).toBe(true) }) }) describe('InstallChannel', () => { it('should create install InstallChannel', async () => { let outputChannel = window.createOutputChannel('test') let channel = new InstallChannel({ isUpdate: true }, outputChannel) channel.start(['a', 'b']) channel.startProgress('a') channel.addMessage('a', 'msg', true) channel.addMessage('a', 'msg', false) channel.finishProgress('a', true) channel.finishProgress('b', false) }) it('should create update InstallChannel', async () => { let outputChannel = window.createOutputChannel('test') let channel = new InstallChannel({ isUpdate: false }, outputChannel) channel.start(['a', 'b']) channel.startProgress('a') channel.finishProgress('a', true) channel.finishProgress('b', false) }) }) ================================================ FILE: src/__tests__/modules/extensions.test.ts ================================================ import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { URI } from 'vscode-uri' import which from 'which' import commands from '../../commands' import { ConfigurationUpdateTarget } from '../../configuration/types' import extensions, { Extensions, toUrl } from '../../extension' import { Disposable, disposeAll } from '../../util' import { writeFile, writeJson } from '../../util/fs' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let tmpfolder: string let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() }) afterAll(async () => { await helper.shutdown() }) afterEach(() => { if (tmpfolder) { fs.rmSync(tmpfolder, { force: true, recursive: true }) tmpfolder = undefined } disposeAll(disposables) }) describe('extensions', () => { it('should convert url', async () => { expect(toUrl('https://github.com/a/b.git#master')).toBe('https://github.com/a/b') expect(toUrl('https://github.com/a/b.git#main')).toBe('https://github.com/a/b') expect(toUrl('url')).toBe('') }) it('should have events', async () => { expect(Extensions).toBeDefined() expect(extensions.onDidLoadExtension).toBeDefined() expect(extensions.onDidActiveExtension).toBeDefined() expect(extensions.onDidUnloadExtension).toBeDefined() expect(extensions.schemes).toBeDefined() expect(extensions.createInstaller('npm', 'id')).toBeDefined() }) it('should not throw with addSchemeProperty', async () => { extensions.addSchemeProperty('', null) }) it('should get update settings', async () => { let settings = extensions.getUpdateSettings() expect(settings.updateCheck).toBe('never') expect(settings.updateUIInTab).toBe(false) expect(settings.silentAutoupdate).toBe(true) let config = workspace.getConfiguration('extensions') await config.update('updateCheck', 'weekly', ConfigurationUpdateTarget.Global) await config.update('updateUIInTab', true, ConfigurationUpdateTarget.Global) await config.update('silentAutoupdate', false, ConfigurationUpdateTarget.Global) settings = extensions.getUpdateSettings() expect(settings.updateCheck).toBe('weekly') expect(settings.updateUIInTab).toBe(true) expect(settings.silentAutoupdate).toBe(false) await config.update('updateCheck', undefined, ConfigurationUpdateTarget.Global) await config.update('updateUIInTab', undefined, ConfigurationUpdateTarget.Global) await config.update('silentAutoupdate', undefined, ConfigurationUpdateTarget.Global) }) it('should toggle auto update', async () => { await commands.executeCommand('extensions.toggleAutoUpdate') let config = workspace.getConfiguration('extensions') expect(config.get('updateCheck')).toBe('daily') await commands.executeCommand('extensions.toggleAutoUpdate') config = workspace.getConfiguration('extensions') expect(config.get('updateCheck')).toBe('never') await config.update('extensions.updateCheck', undefined, ConfigurationUpdateTarget.Global) }) it('should get extensions stat', async () => { process.env.COC_NO_PLUGINS = '1' await extensions.globalExtensions() let stats = await extensions.getExtensionStates() expect(stats.length).toBe(0) process.env.COC_NO_PLUGINS = '0' }) it('should add global extensions', async () => { extensions.states.addExtension('foo', '0.0.1') extensions.states.addExtension('bar', '0.0.1') extensions.modulesFolder = path.join(os.tmpdir(), uuid()) let folder = path.join(extensions.modulesFolder, 'foo') writeJson(path.join(folder, 'package.json'), { name: 'foo', engines: { coc: '>=0.0.1' } }) fs.writeFileSync(path.join(folder, 'index.js'), '') let res = await extensions.globalExtensions() expect(res.length).toBe(1) fs.rmSync(extensions.modulesFolder, { recursive: true }) extensions.states.removeExtension('foo') }) it('should has extension', async () => { let res = extensions.has('test') expect(res).toBe(false) expect(extensions.isActivated('unknown')).toBe(false) let loaded = await helper.doAction('loadedExtensions') expect(loaded).toEqual([]) let stats = await helper.doAction('extensionStats') expect(stats).toBeDefined() }) it('should load global extensions', async () => { extensions.states.addExtension('foo', '0.0.1') let stats = extensions.globalExtensionStats() expect(stats).toEqual([]) extensions.states.removeExtension('foo') process.env.COC_NO_PLUGINS = '1' stats = extensions.globalExtensionStats() expect(stats).toEqual([]) process.env.COC_NO_PLUGINS = '0' }) it('should load extension stats from runtimepath', () => { let f1 = path.join(os.tmpdir(), uuid()) fs.mkdirSync(f1) writeJson(path.join(f1, 'package.json'), { name: 'name', engines: { coc: '>=0.0.1' } }) fs.writeFileSync(path.join(f1, 'index.js'), '') let f2 = path.join(os.tmpdir(), uuid()) fs.mkdirSync(f2) writeJson(path.join(f2, 'package.json'), { name: 'folder', engines: { coc: '>=0.0.1' } }) fs.writeFileSync(path.join(f2, 'index.js'), '') extensions.states.addExtension('folder', '0.0.1') let res = extensions.runtimeExtensionStats([f1, f2]) expect(res.length).toBe(1) expect(res[0].id).toBe('name') extensions.states.removeExtension('folder') fs.rmSync(f1, { recursive: true, force: true }) fs.rmSync(f2, { recursive: true, force: true }) }) it('should force update extensions', async () => { let spy = jest.spyOn(extensions, 'installExtensions').mockImplementation(() => { return Promise.resolve() }) await commands.executeCommand('extensions.forceUpdateAll') spy.mockRestore() }) it('should auto update', async () => { let spy = jest.spyOn(extensions.states, 'shouldUpdate').mockImplementation(() => { return true }) let s = jest.spyOn(extensions, 'updateExtensions').mockImplementation(() => { return Promise.reject(new Error('error on update')) }) await extensions.activateExtensions() spy.mockRestore() s.mockRestore() }) it('should use absolute path for npm', async () => { let res = extensions.npm expect(path.isAbsolute(res)).toBe(true) }) it('should not throw when npm not found', async () => { let spy = jest.spyOn(which, 'sync').mockImplementation(() => { throw new Error('not executable') }) let res = extensions.npm expect(res).toBeNull() await extensions.updateExtensions() spy.mockRestore() }) it('should get all extensions', () => { let list = extensions.all expect(Array.isArray(list)).toBe(true) }) it('should call extension API', async () => { let fn = async () => { await extensions.call('test', 'echo', ['5']) } await expect(fn()).rejects.toThrow(Error) }) it('should catch error when installExtensions', async () => { let spy = jest.spyOn(extensions, 'createInstaller').mockImplementation(() => { return { on: (_key, cb) => { cb('msg', false) }, install: () => { return Promise.resolve({ name: 'name', url: 'http://e', version: '1.0.0' }) } } as any }) let s = jest.spyOn(extensions.states, 'setLocked').mockImplementation(() => { throw new Error('my error') }) await extensions.installExtensions(['abc@1.0.0']) spy.mockRestore() s.mockRestore() }) it('should catch error on updateExtensions', async () => { let spy = jest.spyOn(extensions, 'globalExtensionStats').mockImplementation(() => { return [{ id: 'test' }] as any }) let s = jest.spyOn(extensions, 'createInstaller').mockImplementation(() => { return { on: () => {}, update: () => { return Promise.resolve(path.join(os.tmpdir(), uuid())) } } as any }) await helper.doAction('updateExtensions', true) spy.mockRestore() s.mockRestore() }) it('should update enabled extensions', async () => { let spy = jest.spyOn(extensions, 'globalExtensionStats').mockImplementation(() => { return [{ id: 'test' }, { id: 'global', isLocked: true }, { id: 'disabled', state: 'disabled' }] as any }) let s = jest.spyOn(extensions, 'createInstaller').mockImplementation(() => { return { on: (_key, cb) => { cb('msg', false) }, update: async () => { await helper.wait(1) return '' } } as any }) await extensions.updateExtensions(true, true) spy.mockRestore() s.mockRestore() }) it('should update extensions by url', async () => { let spy = jest.spyOn(extensions, 'globalExtensionStats').mockImplementation(() => { return [{ id: 'test', exotic: true, uri: 'http://example.com' }] as any }) let called = false let s = jest.spyOn(extensions, 'createInstaller').mockImplementation(() => { return { on: (_key, cb) => { cb('msg', false) }, update: async url => { await helper.wait(1) called = true expect(url).toBe('http://example.com') return '' } } as any }) await extensions.updateExtensions() expect(called).toBe(true) spy.mockRestore() s.mockRestore() }) it('should clean unnecessary folders & links', async () => { // create folder and link in modulesFolder let folder = path.join(extensions.modulesFolder, 'test') let link = path.join(extensions.modulesFolder, 'test-link') fs.mkdirSync(folder, { recursive: true }) fs.symlinkSync(folder, link) let stats = extensions.states stats.addExtension('foo', '1.0.0') let extensionFolder = path.join(extensions.modulesFolder, 'foo') fs.mkdirSync(extensionFolder, { recursive: true }) extensions.cleanModulesFolder() expect(fs.existsSync(folder)).toBe(false) expect(fs.existsSync(link)).toBe(false) stats.removeExtension('foo') expect(fs.existsSync(extensionFolder)).toBe(true) fs.rmSync(extensionFolder, { recursive: true }) }) it('should install global extension', async () => { expect(extensions.getExtensionById('coc-omni')).toBeUndefined() let folder = path.join(extensions.modulesFolder, 'coc-omni') let spy = jest.spyOn(extensions, 'createInstaller').mockImplementation(() => { return { on: () => {}, install: async () => { fs.mkdirSync(folder, { recursive: true }) let file = path.join(folder, 'package.json') await writeFile(file, JSON.stringify({ name: 'coc-omni', engines: { coc: '>=0.0.1' }, version: '0.0.1' }, null, 2)) await writeFile(path.join(folder, 'index.js'), 'exports.activate = () => {}') return { name: 'coc-omni', version: '1.0.0', folder } } } as any }) await helper.doAction('installExtensions', 'coc-omni') let item = extensions.getExtension('coc-omni') expect(item).toBeDefined() expect(extensions.getExtensionById('coc-omni')).toBeDefined() expect(item.extension.isActive).toBe(true) expect(extensions.isActivated('coc-omni')).toBe(true) let globals = extensions.globalExtensionStats() expect(globals.length).toBe(1) expect((await extensions.getExtensionStates()).length).toBeGreaterThan(0) spy.mockRestore() await helper.doAction('reloadExtension', 'coc-omni') await helper.doAction('deactivateExtension', 'coc-omni') await helper.doAction('activeExtension', 'coc-omni') await helper.doAction('toggleExtension', 'coc-omni') await helper.doAction('uninstallExtension', 'coc-omni') item = extensions.getExtension('coc-omni') expect(item).toBeUndefined() }) it('should checkRecommendation', async () => { await extensions.checkRecommendation({ name: 'tmp', uri: URI.file(__dirname).toString() }) tmpfolder = path.join(os.tmpdir(), uuid()) let folder = path.join(tmpfolder, '.vim') fs.mkdirSync(folder, { recursive: true }) // fs.mkdirSync(path.join(tmpfolder, '.git'), { recursive: true }) let jsonFile = path.join(folder, 'coc-settings.json') fs.writeFileSync(jsonFile, `{"extensions.recommendations": ["coc-abc", "coc-def"]}`) let returnValue let calledTimes = 0 let spy = jest.spyOn(window, 'showInformationMessage').mockImplementation(() => { calledTimes++ return Promise.resolve(returnValue) }) disposables.push({ dispose: () => { spy.mockRestore() } }) await helper.edit(jsonFile) workspace.workspaceFolderControl.addWorkspaceFolder(tmpfolder, true) await helper.waitValue(() => calledTimes, 1) let called = false let s = jest.spyOn(extensions, 'installExtensions').mockImplementation(() => { called = true return Promise.resolve(undefined) }) disposables.push({ dispose: () => { s.mockRestore() } }) returnValue = { index: 1 } let uri = URI.file(tmpfolder).toString() await extensions.checkRecommendation({ name: 'tmp', uri }) expect(called).toBe(true) returnValue = { index: 2 } await extensions.checkRecommendation({ name: 'tmp', uri }) expect(extensions.states.shouldPrompt(uri)).toBe(false) let curr = calledTimes await extensions.checkRecommendation({ name: 'tmp', uri }) expect(calledTimes).toBe(curr) extensions.states.reset() }) }) ================================================ FILE: src/__tests__/modules/fetch.test.ts ================================================ import fs from 'fs' import http, { Server } from 'http' import os from 'os' import path from 'path' import semver from 'semver' import { URL } from 'url' import { promisify } from 'util' import { v4 as uuid } from 'uuid' import { CancellationTokenSource } from 'vscode-languageserver-protocol' import download, { getEtag, getExtname } from '../../model/download' import fetch, { getAgent, getDataType, getRequestModule, getSystemProxyURI, getText, request, resolveRequestOptions, toPort, toURL } from '../../model/fetch' import helper, { getPort } from '../helper' process.env.NO_PROXY = '*' let port: number beforeAll(async () => { await helper.setup() port = await createServer() }) afterAll(async () => { await helper.shutdown() for (let server of servers) { server.close() } servers = [] }) afterEach(() => { helper.workspace.configurations.reset() }) let servers: Server[] = [] async function createServer(): Promise { let port = await getPort() return await new Promise(resolve => { const server = http.createServer((req, res) => { if (req.url === '/bad_json') { res.writeHead(200, { 'Content-Type': 'application/json;charset=utf8' }) res.end('{"x"') } if (req.url === '/slow') { setTimeout(() => { res.writeHead(200) res.end('abc') }, 50) } if (req.url === '/json') { res.writeHead(200, { 'Content-Type': 'application/json;charset=utf8' }) res.end(JSON.stringify({ result: 'succeed' })) } if (req.url === '/text') { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('text') } if (req.url === '/404') { res.writeHead(404, { 'Content-Type': 'text/plain' }) res.end('not found') } if (req.url === '/reject') { setTimeout(() => { res.socket.destroy(new Error('Rejected')) }, 20) } if (req.url === '/close') { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.write("foo") setTimeout(() => { res.destroy(new Error('closed')) }, 20) } if (req.url === '/binary') { let file = path.join(os.tmpdir(), 'binary_file') if (!fs.existsSync(file)) { res.writeHead(404) res.end() return } let stat = fs.statSync(file) res.setHeader('Content-Length', stat.size) res.setHeader('Etag', '"4c6426ac7ef186464ecbb0d81cbfcb1e"') res.writeHead(200) let stream = fs.createReadStream(file, { highWaterMark: 10 * 1024 }) stream.pipe(res) } if (req.url.startsWith('/zip')) { let zipfile = path.resolve(__dirname, '../test.zip') if (req.url.indexOf('nolength=1') == -1) { let stat = fs.statSync(zipfile) res.setHeader('Content-Length', stat.size) res.setHeader('Content-Disposition', 'attachment') } res.setHeader('Content-Type', 'application/zip') res.writeHead(200) let stream = fs.createReadStream(zipfile, { highWaterMark: 1 * 1024 }) stream.pipe(res) } if (req.url === '/tgz') { res.setHeader('Content-Disposition', 'attachment; filename="file.tgz"') res.setHeader('Content-Type', 'application/octet-stream') let tarfile = path.resolve(__dirname, '../test.tar.gz') let stat = fs.statSync(tarfile) res.setHeader('Content-Length', stat.size) res.writeHead(200) let stream = fs.createReadStream(tarfile) stream.pipe(res) } }) servers.push(server) server.unref() server.listen(port, () => { resolve(port) }) }) } describe('utils', () => { it('should getText', () => { expect(getText({ x: 1 })).toBe('{"x":1}') }) it('should getExtname', () => { let res = getExtname('attachment; x="y"') expect(res).toBeUndefined() }) it('should getPort', async () => { expect(toPort(80, 'http')).toBe(80) expect(toPort('80', 'http')).toBe(80) expect(toPort('x', 'http')).toBe(80) expect(toPort('', 'https')).toBe(443) }) it('should getEtag', () => { expect(getEtag({})).toBeUndefined() expect(getEtag({ etag: '"abc"' })).toBe('abc') expect(getEtag({ etag: 'W/"abc"' })).toBe('abc') expect(getEtag({ etag: 'Wabc"' })).toBeUndefined() }) it('should get data type', () => { expect(getDataType(null)).toBe('null') expect(getDataType(undefined)).toBe('undefined') expect(getDataType('s')).toBe('string') let b = Buffer.from('abc', 'utf8') expect(getDataType(b)).toBe('buffer') expect(getDataType({})).toBe('object') expect(getDataType(new Date())).toBe('unknown') }) it('should getRequestModule', () => { let url = toURL('https://www.baidu.com') expect(getRequestModule(url)).toBeDefined() }) it('should convert to URL', () => { expect(() => { toURL('') }).toThrow() expect(() => { toURL('file:///1') }).toThrow() expect(() => { toURL(undefined) }).toThrow() expect(toURL('https://www.baidu.com').toString()).toBe('https://www.baidu.com/') let u = new URL('http://www.baidu.com') expect(toURL(u)).toBe(u) }) it('should report valid proxy', () => { let agent = getAgent(new URL('http://google.com'), { proxy: 'domain.com:1234' }) expect(agent).toBe(null) agent = getAgent(new URL('http://google.com'), { proxy: 'ftp://domain.com:1234' }) expect(agent).toBe(null) agent = getAgent(new URL('http://google.com'), { proxy: '' }) expect(agent).toBe(null) agent = getAgent(new URL('http://google.com'), { proxy: 'domain.com' }) expect(agent).toBe(null) agent = getAgent(new URL('https://google.com'), { proxy: 'https://domain.com' }) let proxy = (agent as any).proxy expect(proxy.host).toBe('domain.com') expect(proxy.protocol).toBe('https:') expect((agent as any).connectOpts.port).toBe(443) agent = getAgent(new URL('http://google.com'), { proxy: 'http://domain.com', proxyStrictSSL: true }) proxy = (agent as any).proxy expect(proxy.host).toBe('domain.com') expect(proxy.protocol).toBe('http:') expect((agent as any).connectOpts.port).toBe(80) agent = getAgent(new URL('http://google.com'), { proxy: 'https://domain.com:1234' }) proxy = (agent as any).proxy expect(proxy.host).toBe('domain.com:1234') expect(proxy.hostname).toBe('domain.com') expect(proxy.port).toBe('1234') expect((agent as any).connectOpts.port).toBe(1234) agent = getAgent(new URL('http://google.com'), { proxy: 'http://user:pass@domain.com:1234' }) proxy = (agent as any).proxy expect(proxy.host).toBe('domain.com:1234') expect(proxy.hostname).toBe('domain.com') expect(proxy.port).toBe('1234') expect((agent as any).connectOpts.port).toBe(1234) expect(proxy.username).toBe('user') expect(proxy.password).toBe('pass') }) it('should getAgent from proxy', () => { let agent = getAgent(new URL('http://google.com'), { proxy: 'http://user:@domain.com' }) let proxy = (agent as any).proxy expect(proxy.host).toBe('domain.com') expect(proxy.username).toBe('user') expect((agent as any).connectOpts.port).toBe(80) }) it('should getSystemProxyURI', () => { let url = new URL('http://www.example.com') let http_proxy = 'http://127.0.0.1:7070' expect(getSystemProxyURI(url, { NO_PROXY: '*', HTTP_PROXY: http_proxy })).toBeNull() expect(getSystemProxyURI(url, { no_proxy: '*', HTTP_PROXY: http_proxy })).toBeNull() expect(getSystemProxyURI(new URL('http://www.example.com:80'), { NO_PROXY: 'xyz:33,example.com:80', HTTP_PROXY: http_proxy })).toBeNull() expect(getSystemProxyURI(url, { NO_PROXY: 'baidu.com,example.com', HTTP_PROXY: http_proxy })).toBeNull() expect(getSystemProxyURI(url, { HTTP_PROXY: http_proxy })).toBe(http_proxy) expect(getSystemProxyURI(url, { http_proxy })).toBe(http_proxy) expect(getSystemProxyURI(url, {})).toBe(null) url = new URL('https://www.example.com') let https_proxy = 'https://127.0.0.1:7070' expect(getSystemProxyURI(url, { HTTPS_PROXY: https_proxy })).toBe(https_proxy) expect(getSystemProxyURI(url, { https_proxy })).toBe(https_proxy) expect(getSystemProxyURI(url, { HTTP_PROXY: http_proxy })).toBe(http_proxy) expect(getSystemProxyURI(url, { http_proxy })).toBe(http_proxy) expect(getSystemProxyURI(url, {})).toBe(null) }) it('should resolve request options #1', async () => { let file = path.join(os.tmpdir(), `${uuid()}/ca`) fs.mkdirSync(path.dirname(file)) fs.writeFileSync(file, 'ca', 'utf8') helper.updateConfiguration('http.proxyAuthorization', 'authorization') helper.updateConfiguration('http.proxyCA', file) let url = new URL('http://www.example.com:7070') let res = resolveRequestOptions(url, { query: { x: 1 }, method: 'POST', headers: { 'Custom-X': '1' }, user: 'user', password: 'password', timeout: 1000, data: { foo: '1' }, buffer: true, }) expect(res.path).toBe('/?x=1') expect(Buffer.isBuffer(res.ca)).toBe(true) }) it('should resolve request options #2', async () => { let url = new URL('https://abc:123@www.example.com') let res = resolveRequestOptions(url, { user: 'user', data: 'data' }) expect(res.port).toBe(443) expect(res.path).toBe('/') expect(res.auth).toBe('abc:123') }) }) describe('fetch', () => { it('should fetch json', async () => { let res = await fetch(`http://127.0.0.1:${port}/json`, { method: 'POST', data: 'data' }) expect(res).toEqual({ result: 'succeed' }) res = await fetch(`http://127.0.0.1:${port}/json`, { buffer: true }) expect(Buffer.isBuffer(res)).toBe(true) let fn = async () => { await fetch(`http://127.0.0.1:${port}/bad_json`) } await expect(fn()).rejects.toThrow(Error) }) it('should catch error on reject or abnormal response', async () => { let fn = async () => { await fetch(`http://127.0.0.1:${port}/reject`) } await expect(fn()).rejects.toThrow() }) it('should catch abnormal close', async () => { let version = semver.parse(process.version) if (version.major >= 16) { let fn = async () => { await fetch(`http://127.0.0.1:${port}/close`) } await expect(fn()).rejects.toThrow() fn = async () => { await download(`http://127.0.0.1:${port}/close`, { dest: os.tmpdir() }) } await expect(fn()).rejects.toThrow() } }) it('should throw on 404 response', async () => { let fn = async () => { await fetch(`http://127.0.0.1:${port}/404`) } await expect(fn()).rejects.toThrow(Error) }) it('should catch proxy error', async () => { delete process.env.NO_PROXY process.env.HTTP_PROXY = `http://127.0.0.1` let fn = async () => { await fetch(`http://127.0.0.1:${port}/json`) } await expect(fn()).rejects.toThrow() delete process.env.HTTP_PROXY }) it('should throw for ECONNRESET error', async () => { await expect(async () => { let obj: any = {} let url = new URL(`http://127.0.0.1:${port}/text`) let opts = resolveRequestOptions(url, {}) let p = request(url, undefined, opts, undefined, obj) let err: any = new Error('ECONNRESET') err.code = 'ECONNRESET' obj.req.destroy(err) await p }).rejects.toThrow(/ECONNRESET/) }) it('should fetch text', async () => { let res = await fetch(`http://127.0.0.1:${port}/text`) expect(res).toBe('text') let fn = async () => { let port = await getPort() res = await fetch(`http://127.0.0.1:${port}/not_exists`, { timeout: 2000 }) } await expect(fn()).rejects.toThrow() }) it('should throw on timeout', async () => { let fn = async () => { await fetch(`http://127.0.0.1:${port}/slow`, { timeout: 50 }) } await expect(fn()).rejects.toThrow(Error) let url = new URL(`http://127.0.0.1:${port}/slow`) let opts = { method: 'GET', hostname: '127.0.0.1', port, path: '/slow', rejectUnauthorized: true, maxRedirects: 3, headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)', 'Accept-Encoding': 'gzip, deflate' }, timeout: 50, agent: new http.Agent({ keepAlive: true }) } fn = async () => { await request(url, undefined, opts) } await expect(fn()).rejects.toThrow(Error) fn = async () => { await download(url, Object.assign(opts, { dest: os.tmpdir() })) } await expect(fn()).rejects.toThrow(Error) opts.agent.destroy() }) it('should cancel by CancellationToken', async () => { let fn = async () => { let tokenSource = new CancellationTokenSource() let p = fetch(`http://127.0.0.1:${port}/slow`, { timeout: 50 }, tokenSource.token) await helper.wait(1) tokenSource.cancel() await p } await expect(fn()).rejects.toThrow(Error) }) }) describe('download', () => { let binary_file: string let tempdir = path.join(os.tmpdir(), uuid()) beforeAll(async () => { binary_file = path.join(os.tmpdir(), 'binary_file') if (!fs.existsSync(binary_file)) { let data = Buffer.alloc(100 * 1024, 0) await promisify(fs.writeFile)(binary_file, data) } // create binary files }) it('should throw for bad option', async () => { let url = 'https://127.0.0.1' let fn = async () => { await download(url, { dest: 'a/b' }) } await expect(fn()).rejects.toThrow(Error) fn = async () => { await download(url, { dest: __filename }) } await expect(fn()).rejects.toThrow(/not directory/) }) it('should throw on ECONNRESET', async () => { let obj: any = {} let p = download(`http://127.0.0.1:${port}/binary`, { dest: tempdir }, undefined, obj) let err: any = new Error('ECONNRESET') err.code = 'ECONNRESET' await expect(async () => { obj.req.destroy(err) await p }).rejects.toThrow(Error) }) it('should throw when unable to extract', async () => { let url = `http://127.0.0.1:${port}/text` let fn = async () => { await download(url, { dest: tempdir, extract: true }) } await expect(fn()).rejects.toThrow(/extract method/) }) it('should throw for bad response', async () => { let fn = async () => { await download(`http://127.0.0.1:${port}/404`, { dest: tempdir }) } await expect(fn()).rejects.toThrow(Error) fn = async () => { await download(`http://127.0.0.1:${port}/reject`, { dest: tempdir }) } await expect(fn()).rejects.toThrow() fn = async () => { let port = await getPort() await download(`http://127.0.0.1:${port}/not_exists`, { dest: tempdir, timeout: 2000 }) } await expect(fn()).rejects.toThrow() }) it('should throw on timeout', async () => { let fn = async () => { await download(`http://127.0.0.1:${port}/slow`, { dest: tempdir, timeout: 50 }) } await expect(fn()).rejects.toThrow() }) it('should download binary file', async () => { let url = `http://127.0.0.1:${port}/binary` let called = false let res = await download(url, { etagAlgorithm: 'md5', dest: tempdir, onProgress: p => { expect(typeof p).toBe('string') called = true } }) expect(called).toBe(true) let exists = fs.existsSync(res) expect(exists).toBe(true) }) it('should throw when etag check failed', async () => { let url = `http://127.0.0.1:${port}/binary` let called = false let fn = async () => { await download(url, { etagAlgorithm: 'sha256', dest: tempdir, onProgress: p => { expect(typeof p).toBe('string') called = true } }) } await expect(fn()).rejects.toThrow(/Etag check failed/) }) it('should download zip file', async () => { let url = `http://127.0.0.1:${port}/zip` let res = await download(url, { dest: tempdir, extract: true }) let file = path.join(tempdir, 'log.txt') let exists = fs.existsSync(file) expect(exists).toBe(true) res = await download(url + '?nolength=1', { dest: tempdir, extract: true }) exists = fs.existsSync(file) expect(exists).toBe(true) }) it('should download tgz', async () => { let url = `http://127.0.0.1:${port}/tgz` let opts = { dest: tempdir, extract: true, timeout: 3000, strip: 0 } let res = await download(url, opts) let file = path.join(res, 'test.js') let exists = fs.existsSync(file) expect(exists).toBe(true) opts.strip = undefined res = await download(url, opts) expect(res).toBeDefined() }) it('should cancel download by CancellationToken', async () => { let fn = async () => { let tokenSource = new CancellationTokenSource() let p = download(`http://127.0.0.1:${port}/slow`, { dest: tempdir }, tokenSource.token) await helper.wait(10) tokenSource.cancel() await p } await expect(fn()).rejects.toThrow(Error) }) it('should throw on agent error', async () => { delete process.env.NO_PROXY process.env.HTTP_PROXY = `http://127.0.0.1` let fn = async () => { await download(`http://127.0.0.1:${port}/json`, { dest: tempdir }) } await expect(fn()).rejects.toThrow(/using proxy/) delete process.env.HTTP_PROXY process.env.NO_PROXY = '*' fn = async () => { let agent = new http.Agent({ keepAlive: true }) let p = download(`http://127.0.0.1:${port}/slow`, { dest: tempdir, timeout: 50, agent }) await p agent.destroy() } await expect(fn()).rejects.toThrow(/timeout/) }) }) ================================================ FILE: src/__tests__/modules/filter.test.ts ================================================ import { isWhitespaceAtPos, fuzzyScore, isSeparatorAtPos, isPatternInWord, createMatches, FuzzyScorer, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, anyScore, nextTypoPermutation } from '../../util/filter' import * as assert from 'assert' describe('filter functions', () => { function assertMatches(pattern: string, word: string, decoratedWord: string | undefined, filter: FuzzyScorer, opts: { patternPos?: number; wordPos?: number; firstMatchCanBeWeak?: boolean } = {}) { const r = filter(pattern, pattern.toLowerCase(), opts.patternPos || 0, word, word.toLowerCase(), opts.wordPos || 0, { firstMatchCanBeWeak: opts.firstMatchCanBeWeak ?? false, boostFullMatch: true }) assert.ok(!decoratedWord === !r) if (r) { const matches = createMatches(r) let actualWord = '' let pos = 0 for (const match of matches) { actualWord += word.substring(pos, match.start) actualWord += '^' + word.substring(match.start, match.end).split('').join('^') pos = match.end } actualWord += word.substring(pos) assert.strictEqual(actualWord, decoratedWord) } } function assertTopScore(filter: typeof fuzzyScore, pattern: string, expected: number, ...words: string[]) { let topScore = -(100 * 10) let topIdx = 0 for (let i = 0; i < words.length; i++) { const word = words[i] const m = filter(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0) if (m) { const [score] = m if (score > topScore) { topScore = score topIdx = i } } } assert.strictEqual(topIdx, expected, `${pattern} -> actual=${words[topIdx]} <> expected=${words[expected]}`) } test('isWhitespaceAtPos()', () => { expect(isWhitespaceAtPos('abc', -1)).toBe(false) expect(isWhitespaceAtPos('abc', 0)).toBe(false) expect(isWhitespaceAtPos(' bc', 0)).toBe(true) }) test('isSeparatorAtPos()', () => { expect(isSeparatorAtPos('abc', -1)).toBe(false) expect(isSeparatorAtPos('abc', 6)).toBe(false) expect(isSeparatorAtPos('abc', 0)).toBe(false) expect(isSeparatorAtPos(' abc', 0)).toBe(true) expect(isSeparatorAtPos('😕abc', 0)).toBe(true) }) test('isPatternInWord()', () => { const check = (pattern: string, word: string, patternPos = 0, wordPos = 0, result: boolean) => { let res = isPatternInWord(pattern.toLowerCase(), patternPos, pattern.length, word.toLowerCase(), wordPos, word.length) expect(res).toBe(result) } check('abc', 'defabc', 0, 0, true) check('abc', 'defabc', 0, 4, false) check('abc', 'defab/c', 0, 0, true) }) test('fuzzyScore, #23215', function() { assertMatches('tit', 'win.tit', 'win.^t^i^t', fuzzyScore) assertMatches('title', 'win.title', 'win.^t^i^t^l^e', fuzzyScore) assertMatches('WordCla', 'WordCharacterClassifier', '^W^o^r^dCharacter^C^l^assifier', fuzzyScore) assertMatches('WordCCla', 'WordCharacterClassifier', '^W^o^r^d^Character^C^l^assifier', fuzzyScore) }) test('fuzzyScore, #23332', function() { assertMatches('dete', '"editor.quickSuggestionsDelay"', undefined, fuzzyScore) }) test('fuzzyScore, #23190', function() { assertMatches('c:\\do', '& \'C:\\Documents and Settings\'', '& \'^C^:^\\^D^ocuments and Settings\'', fuzzyScore) assertMatches('c:\\do', '& \'c:\\Documents and Settings\'', '& \'^c^:^\\^D^ocuments and Settings\'', fuzzyScore) }) test('fuzzyScore, #23581', function() { assertMatches('close', 'css.lint.importStatement', '^css.^lint.imp^ort^Stat^ement', fuzzyScore) assertMatches('close', 'css.colorDecorators.enable', '^css.co^l^orDecorator^s.^enable', fuzzyScore) assertMatches('close', 'workbench.quickOpen.closeOnFocusOut', 'workbench.quickOpen.^c^l^o^s^eOnFocusOut', fuzzyScore) assertTopScore(fuzzyScore, 'close', 2, 'css.lint.importStatement', 'css.colorDecorators.enable', 'workbench.quickOpen.closeOnFocusOut') }) test('fuzzyScore, #23458', function() { assertMatches('highlight', 'editorHoverHighlight', 'editorHover^H^i^g^h^l^i^g^h^t', fuzzyScore) assertMatches('hhighlight', 'editorHoverHighlight', 'editor^Hover^H^i^g^h^l^i^g^h^t', fuzzyScore) assertMatches('dhhighlight', 'editorHoverHighlight', undefined, fuzzyScore) }) test('fuzzyScore, #23746', function() { assertMatches('-moz', '-moz-foo', '^-^m^o^z-foo', fuzzyScore) assertMatches('moz', '-moz-foo', '-^m^o^z-foo', fuzzyScore) assertMatches('moz', '-moz-animation', '-^m^o^z-animation', fuzzyScore) assertMatches('moza', '-moz-animation', '-^m^o^z-^animation', fuzzyScore) }) test('fuzzyScore', () => { assertMatches('ab', 'abA', '^a^bA', fuzzyScore) assertMatches('ccm', 'cacmelCase', '^ca^c^melCase', fuzzyScore) assertMatches('bti', 'the_black_knight', undefined, fuzzyScore) assertMatches('ccm', 'camelCase', undefined, fuzzyScore) assertMatches('cmcm', 'camelCase', undefined, fuzzyScore) assertMatches('BK', 'the_black_knight', 'the_^black_^knight', fuzzyScore) assertMatches('KeyboardLayout=', 'KeyboardLayout', undefined, fuzzyScore) assertMatches('LLL', 'SVisualLoggerLogsList', 'SVisual^Logger^Logs^List', fuzzyScore) assertMatches('LLLL', 'SVilLoLosLi', undefined, fuzzyScore) assertMatches('LLLL', 'SVisualLoggerLogsList', undefined, fuzzyScore) assertMatches('TEdit', 'TextEdit', '^Text^E^d^i^t', fuzzyScore) assertMatches('TEdit', 'TextEditor', '^Text^E^d^i^tor', fuzzyScore) assertMatches('TEdit', 'Textedit', '^Text^e^d^i^t', fuzzyScore) assertMatches('TEdit', 'text_edit', '^text_^e^d^i^t', fuzzyScore) assertMatches('TEditDit', 'TextEditorDecorationType', '^Text^E^d^i^tor^Decorat^ion^Type', fuzzyScore) assertMatches('TEdit', 'TextEditorDecorationType', '^Text^E^d^i^torDecorationType', fuzzyScore) assertMatches('Tedit', 'TextEdit', '^Text^E^d^i^t', fuzzyScore) assertMatches('ba', '?AB?', undefined, fuzzyScore) assertMatches('bkn', 'the_black_knight', 'the_^black_^k^night', fuzzyScore) assertMatches('bt', 'the_black_knight', 'the_^black_knigh^t', fuzzyScore) assertMatches('ccm', 'camelCasecm', '^camel^Casec^m', fuzzyScore) assertMatches('fdm', 'findModel', '^fin^d^Model', fuzzyScore) assertMatches('fob', 'foobar', '^f^oo^bar', fuzzyScore) assertMatches('fobz', 'foobar', undefined, fuzzyScore) assertMatches('foobar', 'foobar', '^f^o^o^b^a^r', fuzzyScore) assertMatches('form', 'editor.formatOnSave', 'editor.^f^o^r^matOnSave', fuzzyScore) assertMatches('g p', 'Git: Pull', '^Git:^ ^Pull', fuzzyScore) assertMatches('g p', 'Git: Pull', '^Git:^ ^Pull', fuzzyScore) assertMatches('gip', 'Git: Pull', '^G^it: ^Pull', fuzzyScore) assertMatches('gip', 'Git: Pull', '^G^it: ^Pull', fuzzyScore) assertMatches('gp', 'Git: Pull', '^Git: ^Pull', fuzzyScore) assertMatches('gp', 'Git_Git_Pull', '^Git_Git_^Pull', fuzzyScore) assertMatches('is', 'ImportStatement', '^Import^Statement', fuzzyScore) assertMatches('is', 'isValid', '^i^sValid', fuzzyScore) assertMatches('lowrd', 'lowWord', '^l^o^wWo^r^d', fuzzyScore) assertMatches('myvable', 'myvariable', '^m^y^v^aria^b^l^e', fuzzyScore) assertMatches('no', '', undefined, fuzzyScore) assertMatches('no', 'match', undefined, fuzzyScore) assertMatches('ob', 'foobar', undefined, fuzzyScore) assertMatches('sl', 'SVisualLoggerLogsList', '^SVisual^LoggerLogsList', fuzzyScore) assertMatches('sllll', 'SVisualLoggerLogsList', '^SVisua^l^Logger^Logs^List', fuzzyScore) assertMatches('Three', 'HTMLHRElement', undefined, fuzzyScore) assertMatches('Three', 'Three', '^T^h^r^e^e', fuzzyScore) assertMatches('fo', 'barfoo', undefined, fuzzyScore) assertMatches('fo', 'bar_foo', 'bar_^f^oo', fuzzyScore) assertMatches('fo', 'bar_Foo', 'bar_^F^oo', fuzzyScore) assertMatches('fo', 'bar foo', 'bar ^f^oo', fuzzyScore) assertMatches('fo', 'bar.foo', 'bar.^f^oo', fuzzyScore) assertMatches('fo', 'bar/foo', 'bar/^f^oo', fuzzyScore) assertMatches('fo', 'bar\\foo', 'bar\\^f^oo', fuzzyScore) }) test('fuzzyScore (first match can be weak)', function() { assertMatches('Three', 'HTMLHRElement', 'H^TML^H^R^El^ement', fuzzyScore, { firstMatchCanBeWeak: true }) assertMatches('tor', 'constructor', 'construc^t^o^r', fuzzyScore, { firstMatchCanBeWeak: true }) assertMatches('ur', 'constructor', 'constr^ucto^r', fuzzyScore, { firstMatchCanBeWeak: true }) assertTopScore(fuzzyScore, 'tor', 2, 'constructor', 'Thor', 'cTor') }) test('fuzzyScore, many matches', function() { assertMatches( 'aaaaaa', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '^a^a^a^a^a^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', fuzzyScore ) let res = fuzzyScore('a'.repeat(1024), 'a'.repeat(1024), 0, 'word', 'word', 0) expect(res).toBeUndefined() }) test('Freeze when fjfj -> jfjf, https://github.com/microsoft/vscode/issues/91807', function() { assertMatches( 'jfjfj', 'fjfjfjfjfjfjfjfjfjfjfj', undefined, fuzzyScore ) assertMatches( 'jfjfjfjfjfjfjfjfjfj', 'fjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfj', undefined, fuzzyScore ) assertMatches( 'jfjfjfjfjfjfjfjfjfjjfjfjfjfjfjfjfjfjfjjfjfjfjfjfjfjfjfjfjjfjfjfjfjfjfjfjfjfjjfjfjfjfjfjfjfjfjfjjfjfjfjfjfjfjfjfjfj', 'fjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfj', undefined, fuzzyScore ) assertMatches( 'jfjfjfjfjfjfjfjfjfj', 'fJfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfj', 'f^J^f^j^f^j^f^j^f^j^f^j^f^j^f^j^f^j^f^jfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfj', // strong match fuzzyScore ) assertMatches( 'jfjfjfjfjfjfjfjfjfj', 'fjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfj', 'f^j^f^j^f^j^f^j^f^j^f^j^f^j^f^j^f^j^f^jfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfjfj', // any match fuzzyScore, { firstMatchCanBeWeak: true } ) }) test('fuzzyScore, issue #26423', function() { assertMatches('baba', 'abababab', undefined, fuzzyScore) assertMatches( 'fsfsfs', 'dsafdsafdsafdsafdsafdsafdsafasdfdsa', undefined, fuzzyScore ) assertMatches( 'fsfsfsfsfsfsfsf', 'dsafdsafdsafdsafdsafdsafdsafasdfdsafdsafdsafdsafdsfdsafdsfdfdfasdnfdsajfndsjnafjndsajlknfdsa', undefined, fuzzyScore ) }) test('Fuzzy IntelliSense matching vs Haxe metadata completion, #26995', function() { assertMatches('f', ':Foo', ':^Foo', fuzzyScore) assertMatches('f', ':foo', ':^foo', fuzzyScore) }) test('Separator only match should not be weak #79558', function() { assertMatches('.', 'foo.bar', 'foo^.bar', fuzzyScore) }) test('Cannot set property \'1\' of undefined, #26511', function() { const word = new Array(123).join('a') const pattern = new Array(120).join('a') fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0) assert.ok(true) // must not explode }) test('Vscode 1.12 no longer obeys \'sortText\' in completion items (from language server), #26096', function() { assertMatches(' ', ' group', undefined, fuzzyScore, { patternPos: 2 }) assertMatches(' g', ' group', ' ^group', fuzzyScore, { patternPos: 2 }) assertMatches('g', ' group', ' ^group', fuzzyScore) assertMatches('g g', ' groupGroup', undefined, fuzzyScore) assertMatches('g g', ' group Group', ' ^group^ ^Group', fuzzyScore) assertMatches(' g g', ' group Group', ' ^group^ ^Group', fuzzyScore, { patternPos: 1 }) assertMatches('zz', 'zzGroup', '^z^zGroup', fuzzyScore) assertMatches('zzg', 'zzGroup', '^z^z^Group', fuzzyScore) assertMatches('g', 'zzGroup', 'zz^Group', fuzzyScore) }) test('patternPos isn\'t working correctly #79815', function() { assertMatches(':p'.substr(1), 'prop', '^prop', fuzzyScore, { patternPos: 0 }) assertMatches(':p', 'prop', '^prop', fuzzyScore, { patternPos: 1 }) assertMatches(':p', 'prop', undefined, fuzzyScore, { patternPos: 2 }) assertMatches(':p', 'proP', 'pro^P', fuzzyScore, { patternPos: 1, wordPos: 1 }) assertMatches(':p', 'aprop', 'a^prop', fuzzyScore, { patternPos: 1, firstMatchCanBeWeak: true }) assertMatches(':p', 'aprop', undefined, fuzzyScore, { patternPos: 1, firstMatchCanBeWeak: false }) }) test('topScore - fuzzyScore', function() { assertTopScore(fuzzyScore, 'cons', 2, 'ArrayBufferConstructor', 'Console', 'console') assertTopScore(fuzzyScore, 'Foo', 1, 'foo', 'Foo', 'foo') // #24904 assertTopScore(fuzzyScore, 'onMess', 1, 'onmessage', 'onMessage', 'onThisMegaEscape') assertTopScore(fuzzyScore, 'CC', 1, 'camelCase', 'CamelCase') assertTopScore(fuzzyScore, 'cC', 0, 'camelCase', 'CamelCase') // assertTopScore(fuzzyScore, 'cC', 1, 'ccfoo', 'camelCase'); // assertTopScore(fuzzyScore, 'cC', 1, 'ccfoo', 'camelCase', 'foo-cC-bar'); // issue #17836 // assertTopScore(fuzzyScore, 'TEdit', 1, 'TextEditorDecorationType', 'TextEdit', 'TextEditor'); assertTopScore(fuzzyScore, 'p', 4, 'parse', 'posix', 'pafdsa', 'path', 'p') assertTopScore(fuzzyScore, 'pa', 0, 'parse', 'pafdsa', 'path') // issue #14583 assertTopScore(fuzzyScore, 'log', 3, 'HTMLOptGroupElement', 'ScrollLogicalPosition', 'SVGFEMorphologyElement', 'log', 'logger') assertTopScore(fuzzyScore, 'e', 2, 'AbstractWorker', 'ActiveXObject', 'else') // issue #14446 assertTopScore(fuzzyScore, 'workbench.sideb', 1, 'workbench.editor.defaultSideBySideLayout', 'workbench.sideBar.location') // issue #11423 assertTopScore(fuzzyScore, 'editor.r', 2, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace') // assertTopScore(fuzzyScore, 'editor.R', 1, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace'); // assertTopScore(fuzzyScore, 'Editor.r', 0, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace'); assertTopScore(fuzzyScore, '-mo', 1, '-ms-ime-mode', '-moz-columns') // dupe, issue #14861 assertTopScore(fuzzyScore, 'convertModelPosition', 0, 'convertModelPositionToViewPosition', 'convertViewToModelPosition') // dupe, issue #14942 assertTopScore(fuzzyScore, 'is', 0, 'isValidViewletId', 'import statement') assertTopScore(fuzzyScore, 'title', 1, 'files.trimTrailingWhitespace', 'window.title') assertTopScore(fuzzyScore, 'const', 1, 'constructor', 'const', 'cuOnstrul') }) test('Unexpected suggestion scoring, #28791', function() { assertTopScore(fuzzyScore, '_lines', 1, '_lineStarts', '_lines') assertTopScore(fuzzyScore, '_lines', 1, '_lineS', '_lines') assertTopScore(fuzzyScore, '_lineS', 0, '_lineS', '_lines') }) test('HTML closing tag proposal filtered out #38880', function() { assertMatches('\t\t<', '\t\t', '^\t^\t^', fuzzyScore, { patternPos: 0 }) assertMatches('\t\t<', '\t\t', '\t\t^', fuzzyScore, { patternPos: 2 }) assertMatches('\t<', '\t', '\t^', fuzzyScore, { patternPos: 1 }) }) test('fuzzyScoreGraceful', () => { assertMatches('rlut', 'result', undefined, fuzzyScore) assertMatches('rlut', 'result', '^res^u^l^t', fuzzyScoreGraceful) assertMatches('cno', 'console', '^co^ns^ole', fuzzyScore) assertMatches('cno', 'console', '^co^ns^ole', fuzzyScoreGraceful) assertMatches('cno', 'console', '^c^o^nsole', fuzzyScoreGracefulAggressive) assertMatches('cno', 'co_new', '^c^o_^new', fuzzyScoreGraceful) assertMatches('cno', 'co_new', '^c^o_^new', fuzzyScoreGracefulAggressive) }) test('List highlight filter: Not all characters from match are highlighted #66923', () => { assertMatches('foo', 'barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', 'barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_^f^o^o', fuzzyScore) }) test('Autocompletion is matched against truncated filterText to 54 characters #74133', () => { assertMatches( 'foo', 'ffffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', 'ffffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_^f^o^o', fuzzyScore ) assertMatches( 'Aoo', 'Affffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', '^Affffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_f^o^o', fuzzyScore ) assertMatches( 'foo', 'Gffffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', undefined, fuzzyScore ) }) test('"Go to Symbol" with the exact method name doesn\'t work as expected #84787', function() { const match = fuzzyScore(':get', ':get', 1, 'get', 'get', 0, { firstMatchCanBeWeak: true, boostFullMatch: true }) assert.ok(Boolean(match)) }) test('Wrong highlight after emoji #113404', function() { assertMatches('di', '✨div classname="">', '✨^d^iv classname="">', fuzzyScore) assertMatches('di', 'adiv classname="">', 'adiv classname="">', fuzzyScore) }) test('Suggestion is not highlighted #85826', function() { assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScore) assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScoreGracefulAggressive) }) test('IntelliSense completion not correctly highlighting text in front of cursor #115250', function() { assertMatches('lo', 'log', '^l^og', fuzzyScore) assertMatches('.lo', 'log', '^l^og', anyScore) assertMatches('.', 'log', 'log', anyScore) }) test('configurable full match boost', function() { const prefix = 'create' const a = 'createModelServices' const b = 'create' const aBoost = fuzzyScore(prefix, prefix, 0, a, a.toLowerCase(), 0, { boostFullMatch: true, firstMatchCanBeWeak: true }) const bBoost = fuzzyScore(prefix, prefix, 0, b, b.toLowerCase(), 0, { boostFullMatch: true, firstMatchCanBeWeak: true }) assert.ok(aBoost) assert.ok(bBoost) assert.ok(aBoost[0] < bBoost[0]) const aScore = fuzzyScore(prefix, prefix, 0, a, a.toLowerCase(), 0, { boostFullMatch: false, firstMatchCanBeWeak: true }) const bScore = fuzzyScore(prefix, prefix, 0, b, b.toLowerCase(), 0, { boostFullMatch: false, firstMatchCanBeWeak: true }) assert.ok(aScore) assert.ok(bScore) assert.ok(aScore[0] === bScore[0]) }) test('Unexpected suggest highlighting ignores whole word match in favor of matching first letter#147423', function() { assertMatches('i', 'machine/{id}', 'machine/{^id}', fuzzyScore) assertMatches('ok', 'obobobf{ok}/user', '^obobobf{o^k}/user', fuzzyScore) }) test('nextTypoPermutation', () => { expect(nextTypoPermutation('abc', 2)).toBeUndefined() }) test('createMatches()', () => { expect(createMatches(undefined)).toEqual([]) }) }) ================================================ FILE: src/__tests__/modules/floatFactory.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import events from '../../events' import FloatFactoryImpl from '../../model/floatFactory' import snippetManager from '../../snippets/manager' import { Documentation } from '../../types' import helper from '../helper' let nvim: Neovim let floatFactory: FloatFactoryImpl beforeAll(async () => { await helper.setup() nvim = helper.nvim floatFactory = new FloatFactoryImpl(nvim) }) afterAll(async () => { await helper.shutdown() floatFactory.dispose() }) afterEach(async () => { floatFactory.close() await helper.reset() }) describe('FloatFactory', () => { describe('show()', () => { it('should close after create window', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'f' }] let p = floatFactory.show(docs, { shadow: true, focusable: true, rounded: true, border: [1, 1, 1, 1] }) floatFactory.close() await helper.wait(10) let win = floatFactory.window expect(win).toBeNull() }) it('should show window', async () => { expect(floatFactory.window).toBe(null) expect(floatFactory.buffer).toBe(null) expect(floatFactory.bufnr).toBe(0) let docs: Documentation[] = [{ filetype: 'markdown', content: 'f'.repeat(81) }] await floatFactory.show(docs, { rounded: true }) expect(floatFactory.window).toBeDefined() expect(floatFactory.buffer).toBeDefined() let hasFloat = await nvim.call('coc#float#has_float') expect(hasFloat).toBe(1) await floatFactory.show([{ filetype: 'txt', content: '' }]) expect(floatFactory.window).toBe(null) }) it('should close when MenuPopupChanged', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'f'.repeat(81) }] await floatFactory.show(docs, { focusable: true }) await events.fire('BufEnter', [floatFactory.bufnr]) let ev = { row: 21, startcol: 0, index: 0, word: '', height: 1, width: 1, col: 10, size: 1, scrollbar: true, inserted: true, move: false, } await events.fire('MenuPopupChanged', [ev, 22]) await events.fire('MenuPopupChanged', [ev, 20]) expect(floatFactory.window).toBeNull() floatFactory.close() }) it('should create fixed float window', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs, { position: 'fixed', focusable: true, bottom: 1, right: 1 }) let res = await nvim.call('screenpos', [floatFactory.window.id, 1, 1]) as any expect(res).toBeDefined() expect(res.col > 150).toBe(true) expect(res.row > 70).toBe(true) floatFactory.close() }) it('should create window', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'f'.repeat(81) }] await floatFactory.create(docs) expect(floatFactory.window).toBeDefined() }) it('should catch error on create', async () => { let fn = floatFactory.unbind floatFactory.unbind = () => { throw new Error('bad') } let docs: Documentation[] = [{ filetype: 'markdown', content: 'f'.repeat(81) }] await floatFactory.show(docs) floatFactory.unbind = fn let msg = await helper.getCmdline() expect(msg).toMatch('bad') }) it('should show only one window', async () => { await helper.edit() await nvim.setLine('foo') let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await Promise.all([ floatFactory.show(docs), floatFactory.show(docs) ]) let count = 0 let wins = await nvim.windows for (let win of wins) { let isFloat = await win.getVar('float') if (isFloat) count++ } expect(count).toBe(1) }) it('should close window when close called after create', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'f' }] let p = floatFactory.show(docs) await helper.wait(10) floatFactory.close() await p let activated = await floatFactory.activated() expect(activated).toBe(false) }) it('should not create on visual mode', async () => { await helper.createDocument() await nvim.call('cursor', [1, 1]) await nvim.setLine('foo') await nvim.command('normal! v$') let docs: Documentation[] = [{ filetype: 'markdown', content: 'f' }] await floatFactory.show(docs) expect(floatFactory.window).toBe(null) }) it('should allow select mode', async () => { await helper.createDocument() await snippetManager.insertSnippet('${1:foo}') let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs) let { mode } = await nvim.mode expect(mode).toBe('s') await nvim.input('') }) }) describe('checkRetrigger', () => { it('should check retrigger', async () => { expect(floatFactory.checkRetrigger(99)).toBe(false) let bufnr = await nvim.call('bufnr', ['%']) as number let docs: Documentation[] = [{ filetype: 'markdown', content: 'f' }] await floatFactory.show(docs) expect(floatFactory.checkRetrigger(99)).toBe(false) expect(floatFactory.checkRetrigger(bufnr)).toBe(true) }) }) describe('options', () => { it('should config maxHeight and maxWidth', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'f'.repeat(80) + '\nbar', }] await floatFactory.show(docs, { maxWidth: 20, maxHeight: 1 }) let win = floatFactory.window expect(win).toBeDefined() let width = await win.width let height = await win.height expect(width).toBe(19) expect(height).toBe(1) }) it('should set border, title, highlight, borderhighlight, cursorline', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo\nbar' }] await floatFactory.show(docs, { border: [1, 1, 1, 1], title: 'title', highlight: 'Pmenu', borderhighlight: 'MoreMsg', cursorline: true }) let activated = await floatFactory.activated() expect(activated).toBe(true) }) it('should respect prefer top', async () => { let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo\nbar' }] await nvim.call('append', [1, ['', '', '']]) await nvim.command('exe 4') await floatFactory.show(docs, { preferTop: true }) let win = await helper.getFloat() expect(win).toBeDefined() let pos = await nvim.call('nvim_win_get_position', [win.id]) expect(pos).toEqual([1, 0]) }) }) describe('events', () => { it('should hide on BufEnter', async () => { await helper.edit() let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs) await nvim.command(`edit foo`) await helper.waitFor('coc#float#has_float', [], 0) }) it('should not hide when not moved', async () => { let bufnr = await nvim.call('bufnr', ['%']) as number let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs, { focusable: false }) floatFactory._onCursorMoved(false, bufnr, [1, 1]) }) it('should hide on CursorMoved', async () => { let doc = await helper.createDocument() await nvim.input('i') await nvim.setLine('foo') let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs) await helper.waitFloat() floatFactory._onCursorMoved(true, doc.bufnr, [3, 3]) await helper.waitFor('coc#float#has_float', [], 0) }) it('should not hide when cursor position not changed', async () => { await helper.edit() await nvim.setLine('foo') let cursor = await nvim.eval("[line('.'), col('.')]") let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs) floatFactory._onCursorMoved(false, floatFactory.bufnr, [1, 1]) await nvim.call('cursor', cursor) await helper.wait(10) await nvim.call('cursor', cursor) await helper.wait(10) await helper.waitFor('coc#float#has_float', [], 1) }) it('should preserve float when autohide disable and not overlap with pum', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['foo', '', '', '', 'f'], { start: 0, end: -1, strictIndexing: false }) await doc.synchronize() await nvim.call('cursor', [5, 1]) await nvim.input('A') await helper.wait(50) nvim.call('coc#start', [], true) await helper.waitPopup() let docs: Documentation[] = [{ filetype: 'markdown', content: 'foo' }] await floatFactory.show(docs, { preferTop: true, autoHide: false }) let activated = await floatFactory.activated() expect(activated).toBe(true) }) }) }) ================================================ FILE: src/__tests__/modules/fs.test.ts ================================================ import { findUp, isDirectory, findMatch, watchFile, writeJson, loadJson, normalizeFilePath, checkFolder, getFileType, isGitIgnored, readFileLine, readFileLines, fileStartsWith, writeFile, remove, renameAsync, isParentFolder, parentDirs, inDirectory, getFileLineCount, sameFile, lineToLocation, resolveRoot, statAsync, FileType } from '../../util/fs' import { v4 as uuid } from 'uuid' import path from 'path' import fs from 'fs' import os from 'os' import { CancellationToken, CancellationTokenSource, Range } from 'vscode-languageserver-protocol' export function wait(ms: number): Promise { return new Promise(resolve => { setTimeout(() => { resolve(undefined) }, ms) }) } describe('fs', () => { describe('normalizeFilePath()', () => { it('should fs normalizeFilePath', () => { let res = normalizeFilePath('//') expect(res).toBe('/') res = normalizeFilePath('/a/b/') expect(res).toBe('/a/b') }) }) it('should check directory', () => { expect(isDirectory(null)).toBe(false) expect(isDirectory('')).toBe(false) expect(isDirectory(__filename)).toBe(false) expect(isDirectory(process.cwd())).toBe(true) }) it('should watch file', async () => { let filepath = path.join(os.tmpdir(), uuid()) fs.writeFileSync(filepath, 'file', 'utf8') let called = false let disposable = watchFile(filepath, () => { called = true }, true) fs.writeFileSync(filepath, 'new file', 'utf8') await wait(2) disposable.dispose() disposable = watchFile('file_not_exists', () => {}, true) disposable.dispose() }) describe('stat()', () => { it('fs statAsync', async () => { let res = await statAsync(__filename) expect(res).toBeDefined() expect(res.isFile()).toBe(true) }) it('fs statAsync #1', async () => { let res = await statAsync(path.join(__dirname, 'file_not_exist')) expect(res).toBeNull() }) }) describe('loadJson()', () => { it('should loadJson()', () => { let file = path.join(__dirname, 'not_exists.json') expect(loadJson(file)).toEqual({}) }) it('should loadJson with bad format', async () => { let file = path.join(os.tmpdir(), uuid()) fs.writeFileSync(file, 'foo', 'utf8') expect(loadJson(file)).toEqual({}) }) }) describe('writeJson()', () => { it('should writeJson file', async () => { let file = path.join(os.tmpdir(), uuid()) writeJson(file, { x: 1 }) expect(loadJson(file)).toEqual({ x: 1 }) }) it('should create file with folder', async () => { let file = path.join(os.tmpdir(), uuid(), 'foo', 'bar') writeJson(file, { foo: '1' }) expect(loadJson(file)).toEqual({ foo: '1' }) }) }) describe('lineToLocation', () => { it('should not throw when file not exists', async () => { let res = await lineToLocation(path.join(os.tmpdir(), 'not_exists'), 'ab') expect(res).toBeDefined() }) it('should use empty range when not found', async () => { let res = await lineToLocation(__filename, 'a'.repeat(100)) expect(res).toBeDefined() expect(res.range).toEqual(Range.create(0, 0, 0, 0)) }) it('should get location', async () => { let file = path.join(os.tmpdir(), uuid()) fs.writeFileSync(file, '\nfoo\n', 'utf8') let res = await lineToLocation(file, 'foo', 'foo') expect(res.range).toEqual(Range.create(1, 0, 1, 3)) }) }) describe('remove()', () => { it('should remove files', async () => { await remove(path.join(os.tmpdir(), uuid())) let p = path.join(os.tmpdir(), uuid()) fs.writeFileSync(p, 'data', 'utf8') await remove(p) let exists = fs.existsSync(p) expect(exists).toBe(false) await remove(undefined) }) it('should not throw error', async () => { let spy = jest.spyOn(fs, 'rm').mockImplementation(() => { throw new Error('my error') }) let p = path.join(os.tmpdir(), uuid()) await remove(p) spy.mockRestore() }) it('should remove folder', async () => { let f = path.join(os.tmpdir(), uuid()) let p = path.join(f, 'a/b/c') fs.mkdirSync(p, { recursive: true }) await remove(f) let exists = fs.existsSync(f) expect(exists).toBe(false) }) }) describe('getFileType()', () => { it('should get filetype', async () => { let res = await getFileType(__dirname) expect(res).toBe(FileType.Directory) res = await getFileType(__filename) expect(res).toBe(FileType.File) let newPath = path.join(os.tmpdir(), uuid()) fs.symlinkSync(__filename, newPath) res = await getFileType(newPath) expect(res).toBe(FileType.SymbolicLink) fs.unlinkSync(newPath) let spy = jest.spyOn(fs, 'lstat').mockImplementation((...args) => { let cb = args[args.length - 1] as Function return cb(undefined, { isFile: () => { return false }, isDirectory: () => { return false }, isSymbolicLink: () => { return false } }) }) res = await getFileType('__file') expect(res).toBe(FileType.Unknown) spy.mockRestore() }) }) describe('checkFolder()', () => { it('should check file in folder', async () => { let cwd = process.cwd() let res = await checkFolder(cwd, ['package.json']) expect(res).toBe(true) res = await checkFolder(cwd, ['**/schema.json', 'package.json']) expect(res).toBe(true) res = await checkFolder(cwd, []) expect(res).toBe(false) res = await checkFolder(cwd, ['not_exists_fs'], CancellationToken.None) expect(res).toBe(false) res = await checkFolder(os.homedir(), ['not_exists_fs']) expect(res).toBe(false) res = await checkFolder('/a/b/c', ['not_exists_fs']) expect(res).toBe(false) let tokenSource = new CancellationTokenSource() let p = checkFolder(cwd, ['**/a.java'], tokenSource.token) let fn = async () => { tokenSource.cancel() res = await p } await expect(fn()).rejects.toThrow(Error) expect(res).toBe(false) }) }) describe('renameAsync()', () => { it('should rename file', async () => { let id = uuid() let filepath = path.join(os.tmpdir(), id) await writeFile(filepath, id) let dest = path.join(os.tmpdir(), 'bar') await renameAsync(filepath, dest) let exists = fs.existsSync(dest) expect(exists).toBe(true) fs.unlinkSync(dest) }) it('should throw when file does not exist', async () => { let err try { await renameAsync('/foo/bar', '/a') } catch (e) { err = e } expect(err).toBeDefined() }) }) describe('getFileLineCount', () => { it('should throw when file does not exist', async () => { let err try { await getFileLineCount('/foo/bar') } catch (e) { err = e } expect(err).toBeDefined() }) }) describe('sameFile', () => { it('should be casesensitive', () => { expect(sameFile('/a', '/A', false)).toBe(false) expect(sameFile('/a', '/A', true)).toBe(true) }) }) describe('readFileLine', () => { it('should read line', async () => { let res = await readFileLine(__filename, 1) expect(res).toBeDefined() res = await readFileLine(__filename, 9999) expect(res).toBeDefined() expect(res).toBe('') }) it('should throw when file does not exist', async () => { const fn = async () => { await readFileLine(__filename + 'fooobar', 1) } await expect(fn()).rejects.toThrow(Error) }) }) describe('readFileLines', () => { it('should throw when file does not exist', async () => { const fn = async () => { await readFileLines(__filename + 'fooobar', 0, 3) } await expect(fn()).rejects.toThrow(Error) }) it('should read lines', async () => { let res = await readFileLines(__filename, 0, 1) expect(res.length).toBe(2) }) }) describe('fileStartsWith()', () => { it('should check casesensitive case', () => { expect(fileStartsWith('/a/b', '/A', false)).toBe(false) expect(fileStartsWith('/a/b', '/A', true)).toBe(true) }) }) describe('isGitIgnored()', () => { it('should be not ignored', async () => { let res = await isGitIgnored(__filename) expect(res).toBeFalsy() let filepath = path.join(process.cwd(), 'build/index.js') res = await isGitIgnored(filepath) expect(res).toBe(true) }) it('should be ignored', async () => { let res = await isGitIgnored('') let uid = uuid() expect(res).toBe(false) res = await isGitIgnored(path.join(os.tmpdir(), uid)) expect(res).toBe(false) res = await isGitIgnored(path.resolve(__dirname, '../lib/index.js.map')) expect(res).toBe(false) res = await isGitIgnored(__filename) expect(res).toBe(false) let filepath = path.join(os.tmpdir(), uid) fs.writeFileSync(filepath, '', { encoding: 'utf8' }) res = await isGitIgnored(filepath) expect(res).toBe(false) if (fs.existsSync(filepath)) fs.unlinkSync(filepath) }) }) describe('inDirectory', () => { it('should support wildcard', async () => { let res = inDirectory(__dirname, ['**/file_not_exist.json']) expect(res).toBe(false) }) }) describe('parentDirs', () => { it('get parentDirs', () => { let dirs = parentDirs('/a/b/c') expect(dirs).toEqual(['/', '/a', '/a/b']) expect(parentDirs('/')).toEqual(['/']) }) }) describe('isParentFolder', () => { it('check parent folder', () => { expect(isParentFolder('/a/b', '/a/b/')).toBe(false) expect(isParentFolder('/a', '/a/b')).toBe(true) expect(isParentFolder('/a/b', '/a/b')).toBe(false) expect(isParentFolder('/a/b', '/a/b', true)).toBe(true) expect(isParentFolder('//', '/', true)).toBe(true) expect(isParentFolder('/a/b/', '/a/b/c', true)).toBe(true) }) }) describe('resolveRoot', () => { it('resolve root consider root path', () => { let res = resolveRoot(__dirname, ['.git']) expect(res).toMatch('coc.nvim') }) it('should ignore glob pattern', () => { let res = resolveRoot(__dirname, [path.basename(__filename)], undefined, false, false, ["**/__tests__/**"]) expect(res).toBeFalsy() }) it('should ignore glob pattern bottom up', () => { let res = resolveRoot(__dirname, [path.basename(__filename)], undefined, true, false, ["**/__tests__/**"]) expect(res).toBeFalsy() }) it('should resolve from parent folders', () => { let root = path.resolve(__dirname, '../extensions/snippet-sample') let res = resolveRoot(root, ['package.json']) expect(res.endsWith('coc.nvim')).toBe(true) }) it('should resolve from parent folders with bottom-up method', () => { let dir = path.join(os.tmpdir(), 'extensions/snippet-sample') fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(path.resolve(dir, '../package.json'), '{}') let res = resolveRoot(dir, ['package.json'], null, true) expect(res.endsWith('extensions')).toBe(true) fs.rmSync(path.dirname(dir), { recursive: true, force: true }) }) it('should resolve to cwd', () => { let root = path.resolve(__dirname, '../../..') let res = resolveRoot(root, ['package.json'], root, false, true) expect(res).toBe(root) }) it('should resolve to root', () => { let root = path.resolve(__dirname, '../extensions/test/') let res = resolveRoot(root, ['package.json'], root, false, false) expect(res).toBe(path.resolve(__dirname, '../../../')) }) it('should not resolve to home', () => { let res = resolveRoot(__dirname, ['.config'], undefined, false, false, [os.homedir()]) expect(res != os.homedir()).toBeTruthy() }) }) describe('findUp', () => { it('should findMatch by pattern', async () => { let res = findMatch(process.cwd(), ['*.json']) expect(res).toMatch('.json') res = findMatch(process.cwd(), ['*.json_not_exists']) expect(res).toBeUndefined() }) it('findUp by filename', () => { let filepath = findUp('package.json', __dirname) expect(filepath).toMatch('coc.nvim') filepath = findUp('not_exists', __dirname) expect(filepath).toBeNull() }) it('findUp by filenames', async () => { let filepath = findUp(['src'], __dirname) expect(filepath).toMatch('coc.nvim') }) }) }) ================================================ FILE: src/__tests__/modules/fuzzyMatch.test.ts ================================================ import { matchScoreWithPositions } from '../../completion/match' import { FuzzyMatch, matchSpansReverse, FuzzyWasi, initFuzzyWasm } from '../../model/fuzzyMatch' import { getCharCodes } from '../../util/fuzzy' describe('FuzzyMatch', () => { let api: FuzzyWasi beforeAll(async () => { api = await initFuzzyWasm() }) it('should match spans', () => { let f = new FuzzyMatch(api) const verify = (input: string, positions: number[], results: [number, number][], max?: number) => { let arr = f.matchSpans(input, positions, max) let res: [number, number][] = [] for (let item of arr) { res.push(item) } expect(res).toEqual(results) } verify('foobar', [0, 1, 3], [[0, 2], [3, 4]]) verify('foobar', [0], [[0, 1]]) verify('你', [0], [[0, 3]]) verify(' 你', [1], [[1, 4]]) verify('foobar', [0, 2, 3, 4, 1], [[0, 1], [2, 5]]) verify('foobar', [10], []) verify('foobar', [0, 2, 4], [[0, 1], [2, 3], [4, 5]]) verify('foobar', [1, 4], [[1, 2]], 3) verify('foobar', [5], [], 3) }) it('should should matchSpansReverse', () => { const verify = (input: string, positions: number[], results: [number, number][], endIndex?: number, max?: number) => { let arr = matchSpansReverse(input, positions, endIndex, max) let res: [number, number][] = [] for (let item of arr) { res.push(item) } expect(res).toEqual(results) } verify('foobar', [3, 1, 0], [[0, 2], [3, 4]]) verify('foobar', [-1, 2, 3, 1, 0], [[0, 2], [3, 4]], 2) verify('foobar', [0], [[0, 1]]) verify('你', [0], [[0, 3]]) verify(' 你', [1], [[1, 4]]) verify('foobar', [5, 4, 3, 2, 1], [[1, 6]]) verify('foobar', [5], [], 0, 2) verify('foobar', [5, 1], [[1, 2]], 0, 2) verify('f', [0, 1], [], 3) verify('foo', [0, 1, 0, 0, 0], [[0, 1]]) }) it('should createScoreFunction', async () => { let f = new FuzzyMatch(api) let fn = f.createScoreFunction('a', 0) expect(fn).toBeDefined() fn = f.createScoreFunction('a', 0, undefined, 'normal') expect(fn).toBeDefined() fn = f.createScoreFunction('a', 0, undefined, 'aggressive') expect(fn).toBeDefined() fn = f.createScoreFunction('a', 0, undefined, 'any') expect(fn).toBeDefined() let res = fn('asdf') expect(res).toBeDefined() expect(res[2]).toBe(0) let spans: [number, number][] = [] for (let span of f.matchScoreSpans('asdf', res)) { spans.push(span) } expect(spans).toEqual([[0, 1]]) res = fn('asdf') expect(res).toBeDefined() }) it('should throw when not set pattern', () => { let p = new FuzzyMatch(api) let fn = () => { p.match('text') } expect(fn).toThrow(Error) p.free() }) it('should slice pattern when necessary', () => { let pat = 'a'.repeat(258) let p = new FuzzyMatch(api) p.setPattern(pat) let res = p.match('a'.repeat(260)) expect(res).toBeDefined() expect(res.positions.length).toBe(256) }) it('should match empty pattern', () => { let p = new FuzzyMatch(api) p.setPattern('') let res = p.match('foo') expect(res.score).toBe(100) expect(res.positions.length).toBe(0) }) it('should increase content size when necessary', () => { let p = new FuzzyMatch(api) p.setPattern('p') let res = p.match('b'.repeat(2100)) expect(res).toBeUndefined() expect(p.getSizes()[0]).toBe(2101) p.free() }) it('should slice content when necessary', () => { let p = new FuzzyMatch(api) p.setPattern('a') let res = p.match('b'.repeat(40960)) expect(res).toBeUndefined() expect(p.getSizes()[0]).toBe(4097) p.free() p.free() }) it('should fuzzy match ascii', () => { let p = new FuzzyMatch(api) p.setPattern('fb') let res = p.match('fooBar') expect(res).toBeDefined() expect(Array.from(res.positions)).toEqual([0, 3]) res = p.match('foaab') expect(res).toBeDefined() expect(Array.from(res.positions)).toEqual([0, 4]) }) it('should fuzzy match multi byte', () => { let p = new FuzzyMatch(api) p.setPattern('f你好') let res = p.match('foo你好Bar') expect(Array.from(res.positions)).toEqual([0, 3, 4]) }) it('should match highlights', () => { let p = new FuzzyMatch(api) p.setPattern('fb') let res = p.matchHighlights('fooBar', 'Text') expect(res).toBeDefined() expect(res.highlights).toEqual([ { span: [0, 1], hlGroup: 'Text' }, { span: [3, 4], hlGroup: 'Text' } ]) p.setPattern('你') res = p.matchHighlights('吃了吗你', 'Text') expect(res).toBeDefined() expect(res.highlights).toEqual([ { span: [9, 12], hlGroup: 'Text' } ]) res = p.matchHighlights('abc', 'Text') expect(res).toBeUndefined() }) it('should support matchSeq', () => { let p = new FuzzyMatch(api) p.setPattern('foob') let res = p.match('fooBar') expect(Array.from(res.positions)).toEqual([0, 1, 2, 3]) p.setPattern('f b', true) res = p.match('foo bar') expect(Array.from(res.positions)).toEqual([0, 3, 4]) }) it('should better performance', () => { function makeid(length) { let result = '' let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let charactersLength = characters.length for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)) } return result } let arr: string[] = [] for (let i = 0; i < 8000; i++) { arr.push(makeid(50)) } let pat = makeid(3) let p = new FuzzyMatch(api) p.setPattern(pat, true) let ts = Date.now() for (const text of arr) { p.match(text) } // console.log(Date.now() - ts) let codes = getCharCodes(pat) ts = Date.now() for (const text of arr) { matchScoreWithPositions(text, codes) } // console.log(Date.now() - ts) }) }) ================================================ FILE: src/__tests__/modules/highlighter.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import Highlighter from '../../model/highlighter' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) describe('Highlighter', () => { let highlighter: Highlighter beforeEach(() => { highlighter = new Highlighter() }) it('should add line', () => { highlighter.addLine('foo', 'Comment') expect(highlighter.getline(0)).toBe('foo') expect(highlighter.getline(2)).toBe('') expect(highlighter.highlights).toEqual([{ lnum: 0, colStart: 0, colEnd: 3, hlGroup: 'Comment' }]) expect(highlighter.content).toBe('foo') }) it('should add lines', () => { highlighter.addLines(['foo', 'bar']) expect(highlighter.content).toBe('foo\nbar') }) it('should parse ansi highlights', () => { const redOpen = '\x1B[31m' const redClose = '\x1B[39m' highlighter.addLine(redOpen + 'foo' + redClose + 'bar' + redOpen + redClose) expect(highlighter.content).toBe('foobar') }) it('should add texts', () => { highlighter.addTexts([{ text: 'foo' }, { text: 'bar', hlGroup: 'Comment' }]) highlighter.addText('') highlighter.addText(undefined) expect(highlighter.highlights).toEqual([{ lnum: 0, colStart: 3, colEnd: 6, hlGroup: 'Comment' }]) expect(highlighter.content).toBe('foobar') }) it('should render to buffer', async () => { let buf = await nvim.createNewBuffer(true, true) highlighter.addLine('foo', 'Comment') highlighter.addLine('bar') nvim.pauseNotification() highlighter.render(buf) await nvim.resumeNotification() let lines = await buf.lines expect(lines).toEqual(['foo', 'bar']) }) }) ================================================ FILE: src/__tests__/modules/line.test.ts ================================================ import LineBuilder from '../../model/line' describe('LineBuilder', () => { it('should append', async () => { let line = new LineBuilder(true) line.append('') line.append('text') line.append('comment', 'Comment') line.append('nested', undefined, [{ hlGroup: 'Search', offset: 1, length: 2 }]) expect(line.label).toBe('text comment nested') expect(line.highlights).toEqual([ { hlGroup: 'Comment', span: [5, 12] }, { hlGroup: 'Search', span: [14, 16] } ]) let other = new LineBuilder() other.append('text', 'More') line.appendBuilder(other) expect(line.label).toBe('text comment nested text') expect(line.highlights).toEqual([ { hlGroup: 'Comment', span: [5, 12] }, { hlGroup: 'Search', span: [14, 16] }, { hlGroup: 'More', span: [20, 24] } ]) }) it('should append without space', async () => { let line = new LineBuilder(false) line.append('text') let other = new LineBuilder() other.append('text', 'More') line.appendBuilder(other) expect(line.label).toBe('texttext') expect(line.highlights).toEqual([ { hlGroup: 'More', span: [4, 8] } ]) }) }) ================================================ FILE: src/__tests__/modules/logger.test.ts ================================================ import { FileLogger, toTwoDigits, toThreeDigits, textToLogLevel, format, DEFAULT_LOG_LEVEL, LogLevel, stringifyLogLevel } from '../../logger/log' import { createLogger, logger, getTimestamp, resolveLogFilepath, emptyFile } from '../../logger/index' import path from 'path' import fs from 'fs' import os from 'os' import { v4 as uuid } from 'uuid' let filepath: string afterEach(() => { if (fs.existsSync(filepath)) fs.unlinkSync(filepath) }) describe('FileLogger', () => { it('should have DEFAULT_LOG_LEVEL', () => { expect(DEFAULT_LOG_LEVEL).toBeDefined() expect(logger).toBeDefined() }) it('should get LogLevel', () => { expect(stringifyLogLevel('' as any)).toBe('') }) it('should getTimestamp', () => { let res = getTimestamp(new Date()) expect(res).toBeDefined() }) it('should convert digits', () => { expect(toTwoDigits(1)).toBe('01') expect(toTwoDigits(11)).toBe('11') expect(toThreeDigits(1)).toBe('001') expect(toThreeDigits(10)).toBe('010') expect(toThreeDigits(100)).toBe('100') }) it('should get level from text', () => { expect(textToLogLevel('trace')).toBe(LogLevel.Trace) expect(textToLogLevel('debug')).toBe(LogLevel.Debug) expect(textToLogLevel('info')).toBe(LogLevel.Info) expect(textToLogLevel('error')).toBe(LogLevel.Error) expect(textToLogLevel('warning')).toBe(LogLevel.Warning) expect(textToLogLevel('warn')).toBe(LogLevel.Warning) expect(textToLogLevel('off')).toBe(LogLevel.Off) expect(textToLogLevel('')).toBe(LogLevel.Info) }) it('should format', () => { let obj = { x: 1, y: '2', z: {} } as any obj.z.parent = obj let res = format([obj], 2, true, false) expect(res).toBeDefined() res = format([obj]) expect(res).toBeDefined() }) it('should create logger', async () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Trace, { color: false, depth: 2, showHidden: false, userFormatters: true }) let logger = fileLogger.createLogger('scope') logger.log('msg') logger.trace('trace', 'data', {}, 1, true) logger.debug('debug') logger.info('info') logger.warn('warn') logger.error('error') logger.fatal('fatal') logger.mark('mark') await logger.flush() let content = fs.readFileSync(filepath, 'utf8') let lines = content.split(/\n/) expect(lines.length).toBe(8) expect(logger.category).toBeDefined() expect(logger.getLevel()).toBeDefined() }) it('should switch to console', () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Trace, {}) let logger = fileLogger.createLogger('scope') fileLogger.switchConsole() let fn = jest.fn() let spy = jest.spyOn(console, 'error').mockImplementation(() => { fn() }) logger.error('error') spy.mockRestore() expect(fn).toHaveBeenCalled() fn = jest.fn() spy = jest.spyOn(console, 'log').mockImplementation(() => { fn() }) logger.info('info') spy.mockRestore() expect(fn).toHaveBeenCalled() }) it('should enable color', async () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Trace, { color: true }) let logger = fileLogger.createLogger('scope') logger.info('msg', 1, true, { foo: 'bar' }) await logger.flush() let content = fs.readFileSync(filepath, 'utf8') expect(content.indexOf('\x33')).toBeGreaterThan(-1) }) it('should change level', () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Off, {}) fileLogger.setLevel(LogLevel.Debug) fileLogger.setLevel(LogLevel.Debug) }) it('should work with off level', async () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Off, { color: false, depth: 2, showHidden: false, userFormatters: true }) let logger = fileLogger.createLogger('scope') logger.log('msg') logger.trace('trace') logger.debug('debug') logger.info('info') logger.warn('warn') logger.error('error') logger.fatal('fatal') logger.mark('mark') await logger.flush() expect(fs.existsSync(filepath)).toBe(false) }) it('should work without formatter', async () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Trace, { userFormatters: false }) let logger = fileLogger.createLogger('scope') logger.log('msg\n') await logger.flush() let content = fs.readFileSync(filepath, 'utf8') let lines = content.split(/\n/) expect(lines.length).toBe(2) }) it('should use backup file', async () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Trace, { userFormatters: true }) let logger = fileLogger.createLogger('scope') let spy = jest.spyOn(fileLogger, 'shouldBackup').mockImplementation(() => { return true }) for (let i = 0; i < 6; i++) { logger.log(1) } await logger.flush() spy.mockRestore() let newFile = filepath + `_1` expect(fs.existsSync(newFile)).toBe(true) }) it('should not throw on error', async () => { filepath = path.join(os.tmpdir(), uuid()) let fileLogger = new FileLogger(filepath, LogLevel.Trace, { userFormatters: false }) let logger = fileLogger.createLogger('scope') let fn = jest.fn() let s = jest.spyOn(console, 'error').mockImplementation(() => { fn() }) let spy = jest.spyOn(fileLogger, 'shouldBackup').mockImplementation(() => { throw new Error('my error') }) logger.log('msg\n') await logger.flush() expect(fn).toHaveBeenCalled() s.mockRestore() spy.mockRestore() }) it('should create default logger', () => { expect(createLogger()).toBeDefined() }) it('should resolveLogFilepath from env', () => { let filepath = '/tmp/log' process.env.NVIM_COC_LOG_FILE = filepath expect(resolveLogFilepath()).toBe(filepath) process.env.NVIM_COC_LOG_FILE = '' process.env.XDG_RUNTIME_DIR = os.tmpdir() expect(resolveLogFilepath()).toBeDefined() process.env.XDG_RUNTIME_DIR = '/dir_not_exists' expect(resolveLogFilepath()).toBeDefined() process.env.XDG_RUNTIME_DIR = '' expect(resolveLogFilepath()).toBeDefined() }) it('should empty file', async () => { emptyFile('/file_not_exists') filepath = path.join(os.tmpdir(), uuid()) fs.writeFileSync(filepath, 'data', 'utf8') emptyFile(filepath) let content = fs.readFileSync(filepath, 'utf8') expect(content.trim().length).toBe(0) }) }) ================================================ FILE: src/__tests__/modules/map.test.ts ================================================ import * as assert from 'assert' import { LinkedMap, LRUCache, Touch } from '../../util/map' describe('Map', () => { test('LinkedMap - Simple', () => { const map = new LinkedMap() map.trimOld(99) assert.strictEqual(map.first, undefined) assert.strictEqual(map.last, undefined) assert.strictEqual(map.shift(), undefined) map.set('ak', 'av') map.set('bk', 'bv') assert.deepStrictEqual([...map.keys()], ['ak', 'bk']) assert.deepStrictEqual([...map.values()], ['av', 'bv']) assert.strictEqual(map.first, 'av') assert.strictEqual(map.last, 'bv') map.set('ak', 'av', Touch.AsNew) map.set('x', 'av', Touch.AsNew) map.set('y', 'av', Touch.AsOld) map.set('z', 'av', null) map.remove('x') map.get('y', null) map.shift() }) test('LinkedMap - Touch Old one', () => { const map = new LinkedMap() assert.deepStrictEqual(map.isEmpty(), true) map.set('ak', 'av', Touch.AsOld) map.set('ak', 'av') map.set('ak', 'av', Touch.AsOld) assert.deepStrictEqual([...map.keys()], ['ak']) assert.deepStrictEqual([...map.values()], ['av']) assert.deepStrictEqual(map.isEmpty(), false) }) test('LinkedMap - Touch New one', () => { const map = new LinkedMap() map.set('ak', 'av') map.set('ak', 'av', Touch.AsNew) assert.deepStrictEqual([...map.keys()], ['ak']) assert.deepStrictEqual([...map.values()], ['av']) }) test('LinkedMap - Touch Old two', () => { const map = new LinkedMap() map.set('ak', 'av') map.set('bk', 'bv') map.set('bk', 'bv', Touch.AsOld) assert.deepStrictEqual([...map.keys()], ['bk', 'ak']) assert.deepStrictEqual([...map.values()], ['bv', 'av']) }) test('LinkedMap - Touch New two', () => { const map = new LinkedMap() map.set('ak', 'av') map.set('bk', 'bv') map.set('ak', 'av', Touch.AsNew) assert.deepStrictEqual([...map.keys()], ['bk', 'ak']) assert.deepStrictEqual([...map.values()], ['bv', 'av']) }) test('LinkedMap - Touch Old from middle', () => { const map = new LinkedMap() map.set('ak', 'av') map.set('bk', 'bv') map.set('ck', 'cv') map.set('bk', 'bv', Touch.AsOld) assert.deepStrictEqual([...map.keys()], ['bk', 'ak', 'ck']) assert.deepStrictEqual([...map.values()], ['bv', 'av', 'cv']) }) test('LinkedMap - Touch New from middle', () => { const map = new LinkedMap() map.set('ak', 'av') map.set('bk', 'bv') map.set('ck', 'cv') map.set('bk', 'bv', Touch.AsNew) assert.deepStrictEqual([...map.keys()], ['ak', 'ck', 'bk']) assert.deepStrictEqual([...map.values()], ['av', 'cv', 'bv']) }) test('LinkedMap - basics', function() { const map = new LinkedMap() assert.strictEqual(map.size, 0) map.set('1', 1) map.set('2', '2') map.set('3', true) const obj = Object.create(null) map.set('4', obj) const date = Date.now() map.set('5', date) assert.strictEqual(map.size, 5) assert.strictEqual(map.get('1'), 1) assert.strictEqual(map.get('2'), '2') assert.strictEqual(map.get('3'), true) assert.strictEqual(map.get('4'), obj) assert.strictEqual(map.get('5'), date) assert.ok(!map.get('6')) map.delete('6') assert.strictEqual(map.size, 5) assert.strictEqual(map.delete('1'), true) assert.strictEqual(map.delete('2'), true) assert.strictEqual(map.delete('3'), true) assert.strictEqual(map.delete('4'), true) assert.strictEqual(map.delete('5'), true) assert.strictEqual(map.size, 0) assert.ok(!map.get('5')) assert.ok(!map.get('4')) assert.ok(!map.get('3')) assert.ok(!map.get('2')) assert.ok(!map.get('1')) map.set('1', 1) map.set('2', '2') map.set('3', true) assert.ok(map.has('1')) assert.strictEqual(map.get('1'), 1) assert.strictEqual(map.get('2'), '2') assert.strictEqual(map.get('3'), true) map.clear() assert.strictEqual(map.size, 0) assert.ok(!map.get('1')) assert.ok(!map.get('2')) assert.ok(!map.get('3')) assert.ok(!map.has('1')) }) test('LinkedMap - Iterators', () => { const map = new LinkedMap() map.set(1, 1) map.set(2, 2) map.set(3, 3) for (const elem of map.keys()) { assert.ok(elem) } for (const elem of map.values()) { assert.ok(elem) } for (const elem of map.entries()) { assert.ok(elem) } { const keys = map.keys() const values = map.values() const entries = map.entries() map.get(1) keys.next() values.next() entries.next() } { const keys = map.keys() const values = map.values() const entries = map.entries() map.get(1, Touch.AsNew) let exceptions = 0 try { keys.next() } catch (err) { exceptions++ } try { values.next() } catch (err) { exceptions++ } try { entries.next() } catch (err) { exceptions++ } assert.strictEqual(exceptions, 3) } }) test('LinkedMap - LRU Cache simple', () => { const cache = new LRUCache(5) assert.strictEqual(cache.limit, 5) ;[1, 2, 3, 4, 5].forEach(value => cache.set(value, value)) assert.strictEqual(cache.ratio, 1) assert.strictEqual(cache.size, 5) cache.set(6, 6) assert.strictEqual(cache.size, 5) assert.deepStrictEqual([...cache.keys()], [2, 3, 4, 5, 6]) cache.set(7, 7) assert.strictEqual(cache.size, 5) assert.deepStrictEqual([...cache.keys()], [3, 4, 5, 6, 7]) const values: number[] = []; [3, 4, 5, 6, 7].forEach(key => values.push(cache.get(key)!)) assert.deepStrictEqual(values, [3, 4, 5, 6, 7]) cache.ratio = 0.2 cache.ratio = 0.8 cache.limit = 0 assert.strictEqual(cache.size, 0) }) test('LinkedMap - LRU Cache get', () => { const cache = new LRUCache(5); [1, 2, 3, 4, 5].forEach(value => cache.set(value, value)) assert.strictEqual(cache.size, 5) assert.deepStrictEqual([...cache.keys()], [1, 2, 3, 4, 5]) cache.get(3) assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]) cache.peek(4) assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]) const values: number[] = []; [1, 2, 3, 4, 5].forEach(key => values.push(cache.get(key)!)) assert.deepStrictEqual(values, [1, 2, 3, 4, 5]) }) test('LinkedMap - LRU Cache limit', () => { const cache = new LRUCache(10) for (let i = 1; i <= 10; i++) { cache.set(i, i) } assert.strictEqual(cache.size, 10) cache.limit = 5 assert.strictEqual(cache.size, 5) assert.deepStrictEqual([...cache.keys()], [6, 7, 8, 9, 10]) cache.limit = 20 assert.strictEqual(cache.size, 5) for (let i = 11; i <= 20; i++) { cache.set(i, i) } assert.deepStrictEqual(cache.size, 15) const values: number[] = [] for (let i = 6; i <= 20; i++) { values.push(cache.get(i)!) assert.strictEqual(cache.get(i), i) } assert.deepStrictEqual([...cache.values()], values) }) test('LinkedMap - LRU Cache limit with ratio', () => { const cache = new LRUCache(10, 0.5) for (let i = 1; i <= 10; i++) { cache.set(i, i) } assert.strictEqual(cache.size, 10) cache.set(11, 11) assert.strictEqual(cache.size, 5) assert.deepStrictEqual([...cache.keys()], [7, 8, 9, 10, 11]) const values: number[] = []; [...cache.keys()].forEach(key => values.push(cache.get(key)!)) assert.deepStrictEqual(values, [7, 8, 9, 10, 11]) assert.deepStrictEqual([...cache.values()], values) }) test('LinkedMap - toJSON / fromJSON', () => { let map = new LinkedMap() map.set('ak', 'av') map.set('bk', 'bv') map.set('ck', 'cv') const json = map.toJSON() map = new LinkedMap() map.fromJSON(json) let i = 0 map.forEach((value, key) => { if (i === 0) { assert.strictEqual(key, 'ak') assert.strictEqual(value, 'av') } else if (i === 1) { assert.strictEqual(key, 'bk') assert.strictEqual(value, 'bv') } else if (i === 2) { assert.strictEqual(key, 'ck') assert.strictEqual(value, 'cv') } i++ }) i = 0 assert.throws(() => { map.forEach(function(this: object) { assert.deepStrictEqual(this, {}) if (i == 2) { map.set('1', '') } i++ }, {}) }) i = 0 for (let _item of map) { i++ } assert.strictEqual(i, 4) }) test('LinkedMap - delete Head and Tail', function() { const map = new LinkedMap() assert.strictEqual(map.size, 0) map.set('1', 1) assert.strictEqual(map.size, 1) map.delete('1') assert.strictEqual(map.get('1'), undefined) assert.strictEqual(map.size, 0) assert.strictEqual([...map.keys()].length, 0) }) test('LinkedMap - delete Head', function() { const map = new LinkedMap() assert.strictEqual(map.size, 0) map.set('1', 1) map.set('2', 2) assert.strictEqual(map.size, 2) map.delete('1') assert.strictEqual(map.get('2'), 2) assert.strictEqual(map.size, 1) assert.strictEqual([...map.keys()].length, 1) assert.strictEqual([...map.keys()][0], '2') }) test('LinkedMap - delete Tail', function() { const map = new LinkedMap() assert.strictEqual(map.size, 0) map.set('1', 1) map.set('2', 2) assert.strictEqual(map.size, 2) map.delete('2') assert.strictEqual(map.get('1'), 1) assert.strictEqual(map.size, 1) assert.strictEqual([...map.keys()].length, 1) assert.strictEqual([...map.keys()][0], '1') }) test('LinkedMap, - before and after', function(): void { const map = new LinkedMap() map.set('1', 1) map.set('2', 2) assert.strictEqual(map.before('2'), 1) assert.strictEqual(map.before('1'), undefined) assert.strictEqual(map.after('1'), 2) assert.strictEqual(map.after('2'), undefined) assert.strictEqual(map.after('3'), undefined) }) }) ================================================ FILE: src/__tests__/modules/memos.test.ts ================================================ import Memos from '../../model/memos' import os from 'os' import path from 'path' import fs from 'fs' import { loadJson, writeJson } from '../../util/fs' let filepath = path.join(os.tmpdir(), 'test') let memos: Memos beforeEach(() => { memos = new Memos(filepath) }) afterEach(() => { if (fs.existsSync(filepath)) { fs.unlinkSync(filepath) } }) describe('Memos', () => { it('should update and get', async () => { let memo = memos.createMemento('x') await memo.update('foo.bar', 'memo') let res = memo.get('foo.bar') expect(res).toBe('memo') await memo.update('foo.bar', undefined) res = memo.get('foo.bar') expect(res).toBeUndefined() }) it('should get value for key if it does not exist', async () => { let memo = memos.createMemento('y') let res = memo.get('xyz') expect(res).toBeUndefined() }) it('should use defaultValue when it does not exist', async () => { let memo = memos.createMemento('y') let res = memo.get('f.o.o', 'default') expect(res).toBe('default') }) it('should update multiple values', async () => { let memo = memos.createMemento('x') await memo.update('foo', 'x') await memo.update('bar', 'y') expect(memo.get('foo')).toBe('x') expect(memo.get('bar')).toBe('y') }) it('should merge content', async () => { memos.merge(path.join(os.tmpdir(), 'file_not_exists_memos')) let oldPath = path.join(os.tmpdir(), 'old_memos.json') writeJson(oldPath, { old: { release: true } }) memos.merge(oldPath) let obj = loadJson(filepath) as any expect(obj.old.release).toBe(true) expect(fs.existsSync(oldPath)).toBe(false) }) }) ================================================ FILE: src/__tests__/modules/menu.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationTokenSource } from 'vscode-languageserver-protocol' import Menu, { isMenuItem, toIndexText } from '../../model/menu' import helper from '../helper' let nvim: Neovim let menu: Menu beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { if (menu) menu.dispose() await helper.reset() }) describe('Menu', () => { it('should check isMenuItem', () => { expect(isMenuItem(null)).toBe(false) }) it('should get index text', () => { expect(toIndexText(99)).toBe(' ') }) it('should dispose on window close', async () => { await nvim.command('vnew') let currWin = await nvim.window menu = new Menu(nvim, { shortcuts: true, items: [{ text: 'foo' }, { text: 'bar', disabled: true }] }) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) await menu.show() let win = await helper.getFloat() nvim.call('coc#window#close', [currWin.id], true) nvim.call('coc#float#close', [win.id], true) let res = await p expect(res).toBe(-1) }) it('should cancel by ', async () => { menu = new Menu(nvim, { items: [{ text: 'foo' }, { text: 'bar', disabled: true }] }) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) await menu.show() await helper.waitPrompt() await nvim.input('') let res = await p expect(res).toBe(-1) }) it('should cancel before float window shown', async () => { let tokenSource: CancellationTokenSource = new CancellationTokenSource() menu = new Menu(nvim, { items: [{ text: 'foo' }] }, tokenSource.token) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) let promise = menu.show() tokenSource.cancel() await promise let res = await p expect(res).toBe(-1) }) it('should support menu shortcut', async () => { menu = new Menu(nvim, { items: [{ text: 'foo' }, { text: 'bar' }, { text: 'baba' }], shortcuts: true, title: 'Actions' }) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) await menu.show() await helper.waitPrompt() await nvim.input('b') let res = await p expect(res).toBe(1) }) it('should support content', async () => { menu = new Menu(nvim, { items: [{ text: 'foo' }, { text: 'bar' }], content: 'content' }) await menu.show({ confirmKey: '' }) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) let lines = await menu.buffer.lines expect(lines[0]).toBe('content') await nvim.input('') let res = await p expect(res).toBe(0) menu.dispose() }) it('should select by CR', async () => { menu = new Menu(nvim, { items: ['foo', 'bar'] }) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) await menu.show() await helper.waitPrompt() await nvim.input('j') let res = await p expect(res).toBe(1) }) it('should show menu in center', async () => { menu = new Menu(nvim, { items: ['foo', 'bar'], position: 'center' }) await menu.show() expect(menu.buffer).toBeDefined() }) it('should ignore invalid index', async () => { menu = new Menu(nvim, { items: ['foo', 'bar'] }) await menu.show() await helper.waitPrompt() await nvim.input('0') await helper.wait(30) let exists = await nvim.call('coc#float#has_float', []) expect(exists).toBe(1) }) it('should select by index number', async () => { menu = new Menu(nvim, { items: ['foo', 'bar'] }) let p = new Promise(resolve => { menu.onDidClose(v => { resolve(v) }) }) await menu.show() await helper.waitPrompt() await nvim.input('1') let res = await p expect(res).toBe(0) }) it('should choose item after timer', async () => { menu = new Menu(nvim, { items: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] }) await menu.show() let p = new Promise(resolve => { menu.onDidClose(n => { resolve(n) }) }) await helper.waitPrompt() await nvim.input('1') let res = await p expect(res).toBe(0) }) it('should navigate by j, k, g & G', async () => { menu = new Menu(nvim, { items: ['one', 'two', 'three'] }) expect(menu.buffer).toBeUndefined() await menu.onInputChar('session', 'j') await menu.show({ floatHighlight: 'CocFloating', floatBorderHighlight: 'CocFloatBorder' }) let id = await nvim.call('GetFloatWin') as number expect(id).toBeGreaterThan(0) let win = nvim.createWindow(id) await nvim.input('x') await nvim.input('j') await nvim.input('j') await nvim.input('j') await helper.wait(50) let cursor = await win.cursor expect(cursor[0]).toBe(1) await nvim.input('k') await nvim.input('k') await nvim.input('k') await helper.wait(50) cursor = await win.cursor expect(cursor[0]).toBe(1) await nvim.input('G') await helper.wait(50) cursor = await win.cursor expect(cursor[0]).toBe(3) await nvim.input('g') await helper.wait(50) cursor = await win.cursor expect(cursor[0]).toBe(1) await nvim.input('') await nvim.input('') await nvim.input('9') await helper.wait(20) }) it('should select by numbers', async () => { let selected: number menu = new Menu(nvim, { items: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] }) await menu.show() let promise = new Promise(resolve => { menu.onDidClose(n => { selected = n resolve(undefined) }) }) await helper.waitPrompt() await nvim.input('1') await helper.wait(10) await nvim.input('0') await promise expect(selected).toBe(9) }) }) ================================================ FILE: src/__tests__/modules/outputChannel.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import OutputChannel from '../../model/outputChannel' import { wait } from '../../util' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterEach(async () => { await helper.reset() }) afterAll(async () => { await helper.shutdown() }) describe('OutputChannel', () => { test('without nvim', () => { let o = new OutputChannel('f') o.appendLine('foo') o.append('bar') o.show() o.hide() o.clear() }) test('channel name with special characters', async () => { let ch = new OutputChannel("a@b 'c", nvim) ch.show(false, 'edit') let bufname = await nvim.call('bufname', '%') expect(bufname).toBe('output:///a@b%20\'c') let bufnr = await nvim.call('bufnr', ['%']) ch.hide() await helper.wait(10) let loaded = await nvim.call('bufloaded', [bufnr]) expect(loaded).toBe(0) ch.dispose() }) test('outputChannel.show(true)', async () => { await nvim.setLine('foo') let c = new OutputChannel('0', nvim) let bufnr = (await nvim.buffer).id c.show(true) await helper.waitFor('bufnr', ['%'], bufnr) c.hide() c.clear(1) c.dispose() c.append('') c.appendLine('') }) test('outputChannel.keep()', async () => { await nvim.setLine('foo') let c = new OutputChannel('clear', nvim) c.appendLine('foo') c.appendLine('bar') c.show() await helper.wait(10) c.clear(2) let lines = await nvim.call('getbufline', ['output:///clear', 1, '$']) as string[] expect(lines.includes('bar')).toBe(true) }) test('outputChannel.show(false)', async () => { let c = new OutputChannel('1', nvim) let bufnr = (await nvim.buffer).id c.show() await wait(100) let nr = (await nvim.buffer).id expect(bufnr).toBeLessThan(nr) }) test('outputChannel.appendLine()', async () => { let c = new OutputChannel('2', nvim) c.show() await wait(100) let buf = await nvim.buffer c.appendLine('foo') await helper.waitFor('eval', [`join(getbufline(${buf.id},1,'$'),'\n')`], /foo/) }) test('outputChannel.append()', async () => { let c = new OutputChannel('3', nvim) c.show(false) await wait(60) c.append('foo') c.append('bar') await wait(50) let buf = await nvim.buffer await helper.waitFor('eval', [`join(getbufline(${buf.id},1,'$'),'\n')`], /foo/) }) test('outputChannel.clear()', async () => { let c = new OutputChannel('4', nvim) c.show(false) await wait(30) let buf = await nvim.buffer c.appendLine('foo') c.appendLine('bar') await wait(30) c.clear() await wait(30) let lines = await buf.lines let content = lines.join('') expect(content).toBe('') }) }) ================================================ FILE: src/__tests__/modules/picker.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationTokenSource } from 'vscode-languageserver-protocol' import events from '../../events' import Picker, { toPickerItems } from '../../model/picker' import { QuickPickItem } from '../../types' import helper from '../helper' let nvim: Neovim let picker: Picker beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { if (picker) picker.dispose() picker = undefined await helper.reset() }) async function inputChar(ch: string): Promise { await picker.onInputChar('picker', ch) } const items: QuickPickItem[] = [{ label: 'foo' }, { label: 'bar' }] describe('util', () => { it('should convert picker items', () => { expect(toPickerItems([{ label: 'foo' }])).toEqual([{ label: 'foo' }]) expect(toPickerItems(['foo'])).toEqual([{ label: 'foo' }]) }) }) describe('Picker create', () => { it('should show dialog with buttons', async () => { picker = new Picker(nvim, { title: 'title', items: items.concat([{ label: 'three', picked: true }]) }) let winid = await picker.show({ pickerButtons: true }) expect(winid).toBeDefined() let id = await nvim.call('coc#float#get_related', [winid, 'buttons']) expect(id).toBeGreaterThan(0) let res = await nvim.call('sign_getplaced', [picker.buffer.id, { group: 'PopUpCocDialog' }]) expect(res[0].signs).toBeDefined() expect(res[0].signs[0].name).toBe('CocCurrentLine') }) it('should cancel dialog when cancellation token requested', async () => { let tokenSource = new CancellationTokenSource() picker = new Picker(nvim, { title: 'title', items }, tokenSource.token) let winid = await picker.show({ pickerButtons: true, pickerButtonShortcut: true }) expect(winid).toBeDefined() tokenSource.cancel() let win = nvim.createWindow(winid) await helper.waitValue(async () => { return await win.valid }, false) }) it('should cancel dialog without window', async () => { let tokenSource = new CancellationTokenSource() picker = new Picker(nvim, { title: 'title', items }, tokenSource.token) expect(picker.buffer).toBeUndefined() expect(picker.currIndex).toBe(0) await picker.onInputChar('picker', 'i') picker.changeLine(-1) tokenSource.cancel() }) }) describe('Picker key mappings', () => { it('should toggle selection mouse click bracket', async () => { picker = new Picker(nvim, { title: 'title', items }) let winid = await picker.show() await nvim.setVar('mouse_position', [winid, 1, 1]) await nvim.input('') await helper.wait(50) let buf = picker.buffer let lines = await buf.getLines({ start: 0, end: 1, strictIndexing: false }) expect(lines[0]).toMatch(/^\[x\]/) await inputChar('') await events.fire('FloatBtnClick', [picker.bufnr, 0]) }) it('should change current line on mouse click label', async () => { picker = new Picker(nvim, { title: 'title', items }) let winid = await picker.show() await nvim.setVar('mouse_position', [winid, 2, 4]) await nvim.input('') await helper.wait(50) let buf = picker.buffer let res = await nvim.call('sign_getplaced', [buf.id, { group: 'PopUpCocDialog' }]) expect(res[0].signs).toBeDefined() expect(res[0].signs[0].name).toBe('CocCurrentLine') await events.fire('FloatBtnClick', [picker.bufnr, 1]) }) it('should cancel by ', async () => { await helper.createDocument() picker = new Picker(nvim, { title: 'title', items }) let winid = await picker.show({ pickerButtons: true }) expect(winid).toBeDefined() let fn = jest.fn() picker.onDidClose(fn) await picker.onInputChar('picker', '') expect(fn).toHaveBeenCalledTimes(1) }) it('should confirm by ', async () => { await helper.createDocument() let item: QuickPickItem = { label: 'item', description: 'description' } picker = new Picker(nvim, { title: 'title', items: [item].concat(items) }) let winid = await picker.show({ pickerButtons: true }) expect(winid).toBeDefined() let fn = jest.fn() picker.onDidClose(fn) await picker.onInputChar('picker', ' ') await picker.onInputChar('picker', ' ') await picker.onInputChar('picker', 'k') await picker.onInputChar('picker', ' ') await events.fire('FloatBtnClick', [picker.bufnr + 1, 0]) await events.fire('FloatBtnClick', [picker.bufnr, 0]) expect(fn).toHaveBeenCalledTimes(1) }) it('should move cursor by j, k, g & G', async () => { await helper.createDocument() picker = new Picker(nvim, { title: 'title', items }) function getSigns(): Promise { return nvim.call('sign_getplaced', [picker.buffer.id, { group: 'PopUpCocDialog' }]) } let winid = await picker.show({ pickerButtons: true }) await helper.waitFloat() expect(winid).toBeDefined() await nvim.input('j') await helper.wait(100) let res = await getSigns() expect(res[0].signs[0].lnum).toBe(2) await nvim.input('k') await helper.wait(100) res = await getSigns() expect(res[0].signs[0].lnum).toBe(1) await nvim.input('G') await helper.wait(100) res = await getSigns() expect(res[0].signs[0].lnum).toBe(2) await nvim.input('g') await helper.wait(100) res = await getSigns() expect(res[0].signs[0].lnum).toBe(1) }) it('should toggle selection by ', async () => { await helper.createDocument() picker = new Picker(nvim, { title: 'title', items }) let winid = await picker.show({ maxWidth: 60, floatHighlight: 'CocFloating', floatBorderHighlight: 'Normal', rounded: true, confirmKey: 'r', pickerButtons: true }) await helper.waitFloat() expect(winid).toBeDefined() let fn = jest.fn() picker.onDidClose(fn) await inputChar(' ') let lines = await nvim.call('getbufline', [picker.buffer.id, 1]) expect(lines[0]).toMatch('[x]') await inputChar('r') }) it('should scroll forward & backward', async () => { await helper.createDocument() let items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'].map(s => { return { label: s } }) picker = new Picker(nvim, { title: 'title', items }) let event picker.onDidClose(ev => { event = ev }) let winid = await picker.show({ maxHeight: 3 }) expect(winid).toBeDefined() await picker.onInputChar('picker', '') let info = await nvim.call('getwininfo', [winid]) expect(info[0]).toBeDefined() await picker.onInputChar('picker', '') info = await nvim.call('getwininfo', [winid]) expect(info[0]).toBeDefined() await inputChar('') expect(event).toBeUndefined() }) it('should fire selected items on cr', async () => { picker = new Picker(nvim, { title: 'title', items: items.concat([{ label: 'three', picked: true }]) }) let event picker.onDidClose(e => { event = e }) let winid = await picker.show({ pickerButtons: true }) expect(winid).toBeDefined() await inputChar('') expect(event).toEqual([2]) }) }) ================================================ FILE: src/__tests__/modules/plugin.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import path from 'path' import * as vsTypes from 'vscode-languageserver-types' import * as exportObj from '../../index' import Plugin from '../../plugin' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let plugin: Plugin beforeAll(async () => { await helper.setup() nvim = helper.nvim plugin = helper.plugin }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() }) describe('Plugin', () => { it('should check hasAction', () => { expect(plugin.hasAction('NOT_EXISTS')).toBe(false) expect(plugin.hasAction('rename')).toBe(true) }) it('should throw when action exists', () => { expect(() => { plugin.addAction('rename', () => {}) }).toThrow(Error) }) }) describe('exports', () => { it('should exports all types from vscode-languageserver-types', () => { // TODO: LanguageKind added in 3.18, we didn't use this yet // TODO: CodeActionTag added in 3.18, but prpoposed const excludes = [ 'EOL', 'URI', 'TextDocument', 'LanguageKind', 'CodeActionTag', ] let list: string[] = [] for (let key of Object.keys(vsTypes)) { if (typeof exportObj[key] === 'undefined' && !excludes.includes(key)) { list.push(key) } } expect(list.length).toBe(0) for (let key of ['InlineCompletionItem', 'InlineCompletionContext']) { expect(exportObj[key]).toBeDefined() } }) }) describe('help tags', () => { it('should generate help tags', async () => { let root = workspace.pluginRoot let dir = await nvim.call('fnameescape', path.join(root, 'doc')) let res = await nvim.call('execute', `helptags ${dir}`) as string expect(res.length).toBe(0) }) it('should return jumpable', async () => { let jumpable = await helper.plugin.cocAction('snippetCheck', false, true) expect(jumpable).toBe(false) }) it('should show CocInfo', async () => { await helper.doAction('showInfo') let line = await nvim.line expect(line).toMatch('version') }) it('should ensure current document created', async () => { await nvim.command('tabe tmp.js') let res = await helper.plugin.cocAction('ensureDocument') expect(res).toBe(true) let bufnr = await nvim.call('bufnr', ['%']) as number let doc = workspace.getDocument(bufnr) expect(doc).toBeDefined() }) it('should get related information', async () => { let res = await helper.plugin.cocAction('diagnosticRelatedInformation') expect(res).toEqual([]) }) }) ================================================ FILE: src/__tests__/modules/quickpick.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol' import events from '../../events' import QuickPick from '../../model/quickpick' import { QuickPickItem } from '../../types' import { disposeAll } from '../../util' import window from '../../window' import helper from '../helper' export type Item = QuickPickItem | string let nvim: Neovim let disposables: Disposable[] = [] let ns: number beforeAll(async () => { await helper.setup() nvim = helper.nvim ns = await nvim.createNamespace('coc-input-box') }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) disposables = [] }) async function getTitleLine(): Promise { let winids = await nvim.call('coc#float#get_float_win_list') as number[] let winid = Math.min(...winids) let id = await nvim.call('coc#float#get_related', [winid, 'border']) as number let win = nvim.createWindow(id) let buf = await win.buffer let lines = await buf.lines return lines[0] } describe('InputBox', () => { it('should request input', async () => { let winid = await nvim.call('win_getid') let p = window.requestInput('Name') await helper.waitFloat() await nvim.input('bar') let res = await p let curr = await nvim.call('win_getid') expect(curr).toBe(winid) expect(res).toBe('bar') }) it('should use input method of vim', async () => { helper.updateConfiguration('coc.preferences.promptInput', false) let defaultValue = 'default' let p = window.requestInput('Name', defaultValue) await helper.wait(50) await nvim.input('') let res = await p expect(res).toBe(defaultValue) }) it('should return empty string when input empty', async () => { let p = window.requestInput('Name') await helper.wait(30) await nvim.input('') let res = await p expect(res).toBe('') }) it('should emit change event', async () => { let input = await window.createInputBox('', '', {}) disposables.push(input) let curr: string input.onDidChange(text => { curr = text }) await nvim.input('abc') await helper.waitValue((() => { return curr }), 'abc') input.title = 'foo' expect(input.title).toBe('foo') input.loading = true expect(input.loading).toBe(true) input.borderhighlight = 'WarningMsg' expect(input.borderhighlight).toBe('WarningMsg') }) it('should not check bufnr for events', async () => { let input = await window.createInputBox('', undefined, {}) disposables.push(input) let bufnr = input.bufnr let called = false input.onDidChange(() => { called = true }) await events.fire('BufWinLeave', [bufnr + 1]) await events.fire('PromptInsert', ['', bufnr + 1]) await events.fire('TextChangedI', [bufnr + 1, { lnum: 1, col: 1, line: '', changedtick: 0, pre: '' }]) expect(called).toBe(false) expect(input.bufnr).toBeDefined() expect(input.dimension).toBeDefined() }) it('should change input value', async () => { let input = await window.createInputBox('', undefined, {}) disposables.push(input) let called = false input.onDidChange(() => { called = true }) input.value = 'foo' await helper.waitValue(async () => { let lines = await nvim.call('getbufline', [input.bufnr, 1]) as string[] return lines[0] }, 'foo') expect(called).toBe(true) expect(input.value).toBe('foo') }) it('should show and hide placeHolder', async () => { let input = await window.createInputBox('title', undefined, { placeHolder: 'placeHolder' }) disposables.push(input) let buf = nvim.createBuffer(input.bufnr) let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(1) let blocks = markers[0][3].virt_text expect(blocks).toEqual([['placeHolder', 'CocInputBoxVirtualText']]) await nvim.input('a') await helper.waitValue(async () => { let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) return markers.length }, 0) }) }) describe('QuickPick', () => { it('should not thrown when window not shown', async () => { let q = new QuickPick(nvim) q.items = undefined expect(q.winid).toBeUndefined() expect(q.activeItems).toEqual([]) q.title = 'title' expect(q.title).toBe('title') q.loading = true expect(q.loading).toBe(true) q.value = 'value' expect(q.value).toBe('value') expect(q.buffer).toBeUndefined() expect(q.currIndex).toBe(0) q.setCursor(0) q.filterItems('a') q.showFilteredItems() q.toggePicked(0) q.dispose() }) it('should show picker items on filter', async () => { let q = new QuickPick(nvim, {}) q.items = [{ label: 'foo', picked: true }, { label: 'bar', picked: true }, { label: 'asdf', picked: false }] q.canSelectMany = true await q.show() await nvim.input('f') await helper.waitValue(() => { return q.activeItems.length }, 2) expect(q.value).toBe('f') expect(q.selectedItems.length).toBe(2) expect(q.inputBox).toBeDefined() await nvim.input('') await helper.waitValue(() => { return q.selectedItems.length }, 1) q.showFilteredItems() await events.fire('BufWinLeave', [q.buffer.id]) q.dispose() }) }) describe('showQuickPick', () => { async function testQuickPick(items: Item[], canPickMany: boolean, cancel: boolean, res: any) { let p = window.showQuickPick(items, { canPickMany }) await helper.waitFloat() await nvim.input('b') await nvim.input('') await helper.wait(50) if (cancel) { await nvim.input('') } else { await nvim.input('') } let result = await p if (res == null) { expect(result).toBe(res) } else { expect(res).toEqual(res) } } it('should resolve for empty list', async () => { let res = await window.showQuickPick([], { title: 'title' }) expect(res).toBeUndefined() }) it('should resolve undefined when token cancelled', async () => { let tokenSource = new CancellationTokenSource() let token = tokenSource.token tokenSource.cancel() let res = await window.showQuickPick(['foo', 'bar'], undefined, token) expect(res).toBeUndefined() await helper.wait(20) tokenSource = new CancellationTokenSource() token = tokenSource.token let p = window.showQuickPick(['foo', 'bar'], undefined, token) tokenSource.cancel() res = await p expect(res).toBeUndefined() }) it('should show quickfix with items or texts', async () => { await testQuickPick(['foo', 'bar'], false, false, 'bar') await testQuickPick(['foo', 'bar'], true, false, ['bar']) await testQuickPick(['foo', 'bar'], false, true, undefined) let items: QuickPickItem[] = [{ label: 'foo', description: 'desc' }, { label: 'bar', picked: true }] await testQuickPick(items, false, false, { label: 'bar', picked: true }) await testQuickPick(items, true, false, [{ label: 'bar', picked: true }]) }) it('should use title option', async () => { let p = window.showQuickPick(['foo', 'bar'], { title: 'title' }) await helper.waitFloat() let line = await getTitleLine() expect(line).toMatch('title') await nvim.input('') await p }) it('should match on description', async () => { let items: QuickPickItem[] = [{ label: 'foo', description: 'desc' }, { label: 'bar', picked: true }] let p = window.showQuickPick(items, { matchOnDescription: true }) await helper.waitFloat() await nvim.input('d') await helper.wait(10) await nvim.input('') let res = await p expect(res).toBeDefined() }) }) describe('QuickPick configuration', () => { afterEach(() => { helper.workspace.configurations.reset() }) it('should respect width of quickpick', async () => { helper.updateConfiguration('dialog.maxWidth', null) let quickpick = await window.createQuickPick() disposables.push(quickpick) quickpick.items = [{ label: 'foo' }, { label: 'bar' }] quickpick.width = 50 quickpick.value = '' await quickpick.show() let win = nvim.createWindow(quickpick.winid) let width = await win.width expect(width).toBe(50) }) it('should scroll by and ', async () => { helper.updateConfiguration('dialog.maxHeight', 2) let quickpick = await window.createQuickPick() quickpick.value = '' quickpick.items = [{ label: 'one' }, { label: 'two' }, { label: 'three' }] disposables.push(quickpick) await quickpick.show() let winid = quickpick.winid await nvim.input('') await helper.wait(1) await nvim.input('') await helper.waitValue(async () => { let info = await nvim.call('getwininfo', [winid]) return info[0].topline }, 2) await nvim.input('') await nvim.input('') await helper.wait(1) await nvim.input('') await helper.waitValue(async () => { let info = await nvim.call('getwininfo', [winid]) return info[0].topline }, 1) }) it('should respect configurations', async () => { helper.updateConfiguration('dialog.maxWidth', 30) helper.updateConfiguration('dialog.rounded', false) helper.updateConfiguration('dialog.floatHighlight', 'Normal') helper.updateConfiguration('dialog.floatBorderHighlight', 'Normal') helper.updateConfiguration('dialog.maxHeight', 2) let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }, { label: 'two' }, { label: 'three' }] await quickpick.show() let winids = await nvim.call('coc#float#get_float_win_list') as number[] let winid = Math.max(...winids) let win = nvim.createWindow(winid) let h = await win.height expect(h).toBe(2) await nvim.input('') }) }) describe('createQuickPick', () => { it('should throw when unable to open input window', async () => { let fn = nvim.call nvim.call = (...args: any) => { if (args[0] === 'coc#dialog#create_prompt_win') return undefined return fn.apply(nvim, args) } disposables.push(Disposable.create(() => { nvim.call = fn })) let fun = async () => { let quickpick = await window.createQuickPick({ items: [{ label: 'foo' }, { label: 'bar' }], }) await quickpick.show() } await expect(fun()).rejects.toThrow(/Unable to open/) }) it('should throw when unable to open list window', async () => { let fn = nvim.call let spy = jest.spyOn(nvim, 'call').mockImplementation((...args: any) => { if (args[0] === 'coc#dialog#create_list') return undefined return fn.apply(nvim, args) }) let fun = async () => { let quickpick = await window.createQuickPick({ items: [{ label: 'foo' }, { label: 'bar' }], }) disposables.push(quickpick) await quickpick.show() } await expect(fun()).rejects.toThrow(/Unable to open/) spy.mockRestore() await nvim.call('feedkeys', [String.fromCharCode(27), 'in']) }) it('should respect initial value', async () => { let q = await window.createQuickPick() q.items = [{ label: 'foo' }, { label: 'bar' }] q.value = 'value' await q.show() let winids = await nvim.call('coc#float#get_float_win_list') as number[] let winid = Math.min(...winids) let buf = await (nvim.createWindow(winid)).buffer let lines = await buf.lines expect(lines[0]).toBe('value') await nvim.input('') }) it('should change current line by and ', async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one'.repeat(30) }, { label: 'two' }, { label: 'three' }] await quickpick.show() disposables.push(quickpick) let win = nvim.createWindow(quickpick.winid) let height = await win.height expect(height).toBe(4) await nvim.input('') await helper.wait(1) await nvim.input('') await helper.waitValue(() => { return quickpick.currIndex }, 2) await nvim.input('') await helper.wait(1) await nvim.input('') await helper.waitValue(() => { return quickpick.currIndex }, 0) }) it('should toggle selected item by ', async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }, { label: 'two' }, { label: 'three' }] await quickpick.show() disposables.push(quickpick) await nvim.input('') await helper.wait(10) await nvim.input('') await helper.wait(10) await nvim.input('') await helper.waitValue(() => { return quickpick.selectedItems.length }, 0) }) it('should not handle events from other buffer', async () => { let quickpick = await window.createQuickPick({ items: [{ label: 'one' }, { label: 'two' }, { label: 'three' }], }) await quickpick.show() disposables.push(quickpick) await events.fire('BufWinLeave', [quickpick.buffer.id + 1]) await events.fire('PromptKeyPress', [quickpick.buffer.id + 1, 'C-f']) expect(quickpick.currIndex).toBe(0) }) it('should change title', async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }, { label: 'two' }] quickpick.title = 'from' disposables.push(quickpick) quickpick.title = 'to' expect(quickpick.title).toBe('to') await quickpick.show() let line = await getTitleLine() expect(line).toMatch(/to/) }) it('should change loading', async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }, { label: 'two' }] disposables.push(quickpick) await quickpick.show() quickpick.loading = true expect(quickpick.loading).toBe(true) quickpick.loading = false expect(quickpick.loading).toBe(false) }) it('should change items', async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }, { label: 'two' }] await quickpick.show() disposables.push(quickpick) quickpick.onDidChangeValue(val => { if (val == '>') { quickpick.items = [{ label: 'three' }] } }) await nvim.input('>') await helper.waitValue(async () => { let lines = await quickpick.buffer.lines return lines }, ['three']) }) it('should change activeItems', async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }] disposables.push(quickpick) await quickpick.show() quickpick.onDidChangeValue(val => { if (val == 'f') { quickpick.activeItems = [{ label: 'foo', description: 'description' }, { label: 'foot' }, { label: 'bar' }] } }) await nvim.input('f') await helper.waitValue(async () => { let lines = await quickpick.buffer.lines return lines }, ['foo description', 'foot', 'bar']) }) it('should check InputListSelect', async () => { const createQuickPick = async () => { let quickpick = await window.createQuickPick() quickpick.items = [{ label: 'one' }] disposables.push(quickpick) await quickpick.show() return quickpick } for (let val of [-1, 1]) { let quickpick = await createQuickPick() let called = false quickpick.onDidFinish(() => { called = true }) await events.fire('InputListSelect', [val]) await helper.waitValue(() => { return called }, true) } }) }) ================================================ FILE: src/__tests__/modules/regions.test.ts ================================================ import Regions from '../../model/regions' describe('Regions', () => { it('should add #1', async () => { let r = new Regions() r.add(1, 2) r.add(2, 1) expect(r.current).toEqual([1, 2]) }) it('should add #2', async () => { let r = new Regions() r.add(3, 4) r.add(1, 5) expect(r.current).toEqual([1, 5]) }) it('should add #3', async () => { let r = new Regions() r.add(2, 3) r.add(1, 2) expect(r.current).toEqual([1, 3]) }) it('should add #4', async () => { let r = new Regions() r.add(2, 5) r.add(3, 4) expect(r.current).toEqual([2, 5]) }) it('should add #5', async () => { let r = new Regions() r.add(3, 4) r.add(1, 5) expect(r.current).toEqual([1, 5]) }) it('should add #6', async () => { let r = new Regions() r.add(1, 2) r.add(3, 5) expect(r.current).toEqual([1, 5]) r.add(1, 8) expect(r.current).toEqual([1, 8]) }) it('should add #7', async () => { let r = new Regions() r.add(1, 2) r.add(1, 5) expect(r.current).toEqual([1, 5]) r.add(9, 10) r.add(5, 6) expect(r.current).toEqual([1, 6, 9, 10]) }) it('should check range', async () => { let r = new Regions() r.add(1, 2) r.add(1, 5) expect(r.has(3, 5)).toBe(true) expect(r.has(3, 6)).toBe(false) r.add(6, 8) expect(r.has(1, 8)).toBe(true) }) it('should get range', async () => { let r = new Regions() r.add(1, 2) r.add(1, 5) expect(r.isEmpty).toBe(false) expect(r.getRange(8)).toBeUndefined() expect(r.getRange(9)).toBeUndefined() expect(r.getRange(1)).toEqual([1, 5]) expect(r.getRange(5)).toEqual([1, 5]) }) it('should get uncovered range', async () => { let r = new Regions() expect(r.toUncoveredSpan([1, 2], 3, 10)).toEqual([0, 5]) r.add(0, 5) expect(r.toUncoveredSpan([1, 2], 3, 10)).toBeUndefined() r.add(8, 10) expect(r.toUncoveredSpan([4, 6], 3, 20)).toEqual([5, 8]) }) it('should merge spans', async () => { expect(Regions.mergeSpans([[0, 1], [1, 2]])).toEqual([[0, 2]]) expect(Regions.mergeSpans([[0, 1], [2, 3]])).toEqual([[0, 1], [2, 3]]) expect(Regions.mergeSpans([[2, 3], [0, 1]])).toEqual([[2, 3], [0, 1]]) expect(Regions.mergeSpans([[1, 4], [0, 5]])).toEqual([[0, 5]]) expect(Regions.mergeSpans([[1, 4], [2, 3]])).toEqual([[1, 4]]) expect(Regions.mergeSpans([[1, 2], [2, 3], [3, 4]])).toEqual([[1, 4]]) }) }) ================================================ FILE: src/__tests__/modules/sandbox/log.js ================================================ const {wait, nvim} = require('coc.nvim') console.log('log') console.debug('debug') console.info('info') console.error('error') console.warn('warn') module.exports = () => { return {wait, nvim} } ================================================ FILE: src/__tests__/modules/semanticTokensBuilder.test.ts ================================================ import { SemanticTokensBuilder } from '../../model/semanticTokensBuilder' import { Range, SemanticTokensLegend } from 'vscode-languageserver-protocol' function toArr(uint32Arr: ReadonlyArray): number[] { const r = [] for (let i = 0, len = uint32Arr.length; i < len; i++) { r[i] = uint32Arr[i] } return r } function deepStrictEqual(one: any, two: any): void { expect(one).toEqual(two) } describe('SemanticTokensBuilder', () => { it('should build SemanticTokensBuilder simple', () => { const builder = new SemanticTokensBuilder() builder.push(1, 0, 5, 1, 1) builder.push(1, 10, 4, 2, 2) builder.push(2, 2, 3, 2, 2) deepStrictEqual(toArr(builder.build().data), [ 1, 0, 5, 1, 1, 0, 10, 4, 2, 2, 1, 2, 3, 2, 2 ]) }) it('should throw for bad arguments', async () => { const builder = new SemanticTokensBuilder() expect(() => { builder.push(undefined, undefined, undefined, undefined) }).toThrow(Error) expect(() => { builder.push(Range.create(0, 0, 0, 3), '') }).toThrow(Error) Object.assign(builder, { _hasLegend: true }) expect(() => { builder.push(Range.create(0, 0, 1, 3), '') }).toThrow(Error) expect(() => { builder.push(Range.create(0, 0, 0, 3), '') }).toThrow(Error) }) it('should build SemanticTokensBuilder no modifier', () => { const builder = new SemanticTokensBuilder() builder.push(1, 0, 5, 1) builder.push(1, 10, 4, 2) builder.push(2, 2, 3, 2) deepStrictEqual(toArr(builder.build().data), [ 1, 0, 5, 1, 0, 0, 10, 4, 2, 0, 1, 2, 3, 2, 0 ]) }) it('should build SemanticTokensBuilder out of order 1', () => { const builder = new SemanticTokensBuilder() builder.push(2, 0, 5, 1, 1) builder.push(2, 10, 1, 2, 2) builder.push(2, 15, 2, 3, 3) builder.push(1, 0, 4, 4, 4) deepStrictEqual(toArr(builder.build().data), [ 1, 0, 4, 4, 4, 1, 0, 5, 1, 1, 0, 10, 1, 2, 2, 0, 5, 2, 3, 3 ]) }) it('SemanticTokensBuilder out of order 2', () => { const builder = new SemanticTokensBuilder() builder.push(2, 10, 5, 1, 1) builder.push(2, 2, 4, 2, 2) deepStrictEqual(toArr(builder.build().data), [ 2, 2, 4, 2, 2, 0, 8, 5, 1, 1 ]) }) test('SemanticTokensBuilder with legend', () => { const legend: SemanticTokensLegend = { tokenTypes: ['aType', 'bType', 'cType', 'dType'], tokenModifiers: ['mod0', 'mod1', 'mod2', 'mod3', 'mod4', 'mod5'] } const builder = new SemanticTokensBuilder(legend) builder.push(Range.create(1, 0, 1, 5), 'bType') builder.push(Range.create(2, 0, 2, 4), 'cType', ['mod0', 'mod5']) builder.push(Range.create(3, 0, 3, 3), 'dType', ['mod2', 'mod4']) deepStrictEqual(toArr(builder.build().data), [ 1, 0, 5, 1, 0, 1, 0, 4, 2, 1 | (1 << 5), 1, 0, 3, 3, (1 << 2) | (1 << 4) ]) expect(() => { builder.push(Range.create(3, 0, 3, 3), 'dType', ['mod2', 'mod4', 'mod10']) }).toThrow(Error) }) }) ================================================ FILE: src/__tests__/modules/server.js ================================================ "use strict" Object.defineProperty(exports, "__esModule", {value: true}) const node_1 = require("vscode-languageserver/node") const connection = (0, node_1.createConnection)() let notified = false connection.onInitialize((_params) => { return { capabilities: {} } }) connection.onRequest('request', (param) => { return param.value + 1 }) connection.onNotification('notification', () => { notified = true }) connection.onRequest('notified', () => { return {notified} }) connection.onRequest('triggerRequest', async () => { await connection.sendRequest('request') }) connection.onNotification('triggerNotification', async () => { await connection.sendNotification('notification', {x: 1}) }) connection.listen() ================================================ FILE: src/__tests__/modules/services.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import net from 'net' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import { LanguageClient, RevealOutputChannelOn, ServerOptions, State, TransportKind } from '../../language-client' import services, { convertState, documentSelectorToLanguageIds, getDocumentSelector, getForkOptions, getLanguageServerOptions, getRevealOutputChannelOn, getSpawnOptions, getStateName, getTransportKind, isValidServerConfig, LanguageServerConfig, ServiceStat, stateString } from '../../services' import { disposeAll } from '../../util' import { Workspace } from '../../workspace' import events from '../../events' import helper from '../helper' import window from '../../window' let nvim: Neovim let disposables: Disposable[] = [] let workspace: Workspace const serverModule = path.join(__dirname, 'server.js') beforeAll(async () => { await helper.setup() workspace = helper.workspace nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) }) function toConfig(c: Partial): LanguageServerConfig { if (!c.filetypes) { c.filetypes = ['vim'] } return c as LanguageServerConfig } describe('services', () => { describe('functions', () => { it('should convertState', async () => { expect(convertState(null as any)).toBeUndefined() }) it('should check valid server config', async () => { expect(isValidServerConfig('name', {} as any)).toBe(false) expect(isValidServerConfig('name', { module: [] } as any)).toBe(false) expect(isValidServerConfig('name', { command: [] } as any)).toBe(false) expect(isValidServerConfig('name', { transport: '' } as any)).toBe(false) expect(isValidServerConfig('name', { transportPort: 'ab' } as any)).toBe(false) expect(isValidServerConfig('name', { filetypes: '' } as any)).toBe(false) expect(isValidServerConfig('name', { additionalSchemes: '' } as any)).toBe(false) expect(isValidServerConfig('name', { additionalSchemes: [1] } as any)).toBe(false) expect(isValidServerConfig('name', { module: 'module', filetypes: ['vim'] } as any)).toBe(true) }) it('should get state name', async () => { expect(getStateName(ServiceStat.Initial)).toBe('init') expect(getStateName(ServiceStat.Running)).toBe('running') expect(getStateName(ServiceStat.Starting)).toBe('starting') expect(getStateName(ServiceStat.StartFailed)).toBe('startFailed') expect(getStateName(ServiceStat.Stopping)).toBe('stopping') expect(getStateName(ServiceStat.Stopped)).toBe('stopped') expect(getStateName(null as any)).toBe('unknown') }) it('should use languageserver config from workspace folder', async () => { let folder = path.join(os.tmpdir(), uuid()) fs.mkdirSync(path.join(folder, '.vim'), { recursive: true }) let configFile = path.join(folder, '.vim/coc-settings.json') fs.writeFileSync(configFile, '{"languageserver": {"foo": {"command":"bar", "filetypes": ["vim"]}, "bar": {}}}') let uri = URI.file(path.join(folder, 't')).toString() let added = workspace.configurations.locateFolderConfigution(uri) expect(added).toBe(true) let w = workspace.workspaceFolderControl w.addWorkspaceFolder(folder, true) let s = services.getService('foo') let spy = jest.spyOn(window as any, 'showErrorMessage').mockImplementation(() => { return Promise.resolve() }) expect(s).toBeDefined() await s.restart() spy.mockRestore() w.removeWorkspaceFolder(folder) }) it('should get stateString', async () => { expect(stateString(State.Stopped)).toBe('stopped') expect(stateString(State.Running)).toBe('running') expect(stateString(State.Starting)).toBe('starting') expect(stateString(null as any)).toBe('unknown') }) it('should getSpawnOptions', async () => { expect(getSpawnOptions(toConfig({ cwd: process.cwd() }))).toBeDefined() expect(getSpawnOptions(toConfig({ cwd: process.cwd(), detached: true, shell: true, env: {} }))).toBeDefined() }) it('should getForkOptions', async () => { expect(getForkOptions(toConfig({ cwd: process.cwd() }))).toBeDefined() expect(getForkOptions(toConfig({ cwd: process.cwd(), execArgv: [], env: {} }))).toBeDefined() }) it('should getTransportKind', async () => { expect(getTransportKind(toConfig({}))).toBe(TransportKind.ipc) expect(getTransportKind(toConfig({ transport: 'ipc' }))).toBe(TransportKind.ipc) expect(getTransportKind(toConfig({ transport: 'stdio' }))).toBe(TransportKind.stdio) expect(getTransportKind(toConfig({ transport: 'pipe' }))).toBe(TransportKind.pipe) expect(getTransportKind(toConfig({ transport: 'socket', transportPort: 3300 }))).toEqual({ kind: TransportKind.socket, port: 3300 }) }) it('should getDocumentSelector', async () => { expect(getDocumentSelector(undefined, [])).toEqual([{ scheme: 'file' }, { scheme: 'untitled' }]) expect(getDocumentSelector(['vim'], []).length).toBe(2) }) it('should getRevealOutputChannelOn', async () => { expect(getRevealOutputChannelOn('error')).toBe(RevealOutputChannelOn.Error) expect(getRevealOutputChannelOn('info')).toBe(RevealOutputChannelOn.Info) expect(getRevealOutputChannelOn('warn')).toBe(RevealOutputChannelOn.Warn) expect(getRevealOutputChannelOn('never')).toBe(RevealOutputChannelOn.Never) expect(getRevealOutputChannelOn('')).toBe(RevealOutputChannelOn.Never) }) it('should getLanguageServerOptions', async () => { expect(getLanguageServerOptions('x', 'y', {} as any)).toBe(null) expect(getLanguageServerOptions('x', 'y', { filetypes: ['vim'] })).toBe(null) expect(getLanguageServerOptions('x', 'y', toConfig({ module: 'not_exists' }))).toBe(null) expect(getLanguageServerOptions('x', 'y', toConfig({ module: __filename, maxRestartCount: 1 }))).toBeDefined() expect(getLanguageServerOptions('x', 'y', toConfig({ module: __filename, runtime: process.execPath }))).toBeDefined() expect(getLanguageServerOptions('x', 'y', toConfig({ command: 'cmd', args: [], disableWorkspaceFolders: true, disableSnippetCompletion: true } as any))).toBeDefined() expect(getLanguageServerOptions('x', 'y', toConfig({ command: 'cmd', ignoredRootPaths: ['/foo'], initializationOptions: {} }))).toBeDefined() }) it('should use socket port for language server #1', async () => { let opts = getLanguageServerOptions('x', 'y', toConfig({ port: 3300, host: '127.0.0.1' })) let fn = opts[1] as Function await expect(fn()).rejects.toThrow(Error) }) it('should use socket port for language server #2', async () => { let connected = false let s let server = net.createServer(socket => { connected = true s = socket }) server.listen(12580, '127.0.0.1') let opts = getLanguageServerOptions('x', 'y', toConfig({ port: 12580 })) let fn = opts[1] as Function let res = await fn() await helper.wait(30) expect(res).toBeDefined() expect(connected).toBe(true) s.destroy() server.close() }) it('should documentSelectorToLanguageIds', async () => { expect(documentSelectorToLanguageIds(['vim'])).toEqual(['vim']) }) }) describe('getServiceStats()', () => { it('should get services', async () => { let res = await helper.doAction('services') expect(res).toBeDefined() }) }) describe('toggle()', () => { it('should throw when service not found ', async () => { let fn = async () => { await helper.doAction('toggleService', 'id') } await expect(fn()).rejects.toThrow(Error) }) it('should toggle language client state', async () => { const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, { documentSelector: [{ language: 'vim', scheme: 'file' }] }) let d = services.registerLanguageClient(client) disposables.push(d) let p = services.toggle('test') void services.toggle('test') await p let s = services.getService('test') expect(s.state).toBe(ServiceStat.Running) d.dispose() }) }) describe('start()', () => { it('should delay start when not plugin not ready', async () => { Object.assign(events, { _ready: false }) let called = false services.tryStartService({ id: 'test', start: () => { called = true } } as any) let started = false services.tryStartService({ id: 'test', state: ServiceStat.Initial, selector: [{ language: '*' }], start: () => { started = true } } as any) await events.fire('ready', []) expect(called).toBe(false) expect(started).toBe(true) }) it('should start language client on by document', async () => { const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, { documentSelector: [{ language: 'vim', scheme: 'file' }] }) disposables.push(services.registerLanguageClient(client)) let document = TextDocument.create('file:///1', 'vim', 1, '') await services.start(document) await services.start(TextDocument.create('file:///2', 'java', 1, '')) let s = services.getService('test') expect(s.state).toBe(ServiceStat.Running) let code = `call coc#on_notify('test', 'notification', { -> execute('let g:called = 1')})` await nvim.exec(code) await helper.doAction('registerNotification', 'test', 'notification') await client.sendNotification('triggerNotification') await helper.waitValue(() => { return nvim.getVar('called') }, 1) }) }) describe('stop()', () => { it('should not throw when service not found ', async () => { await services.stop('id') }) }) describe('shouldStart()', () => { it('should start when document matches', async () => { await helper.edit('t.vim') const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, { documentSelector: [{ language: 'vim', scheme: 'file' }] }) disposables.push(services.registerLanguageClient(client)) services.register({ id: 'test' } as any) await helper.waitValue(() => { return client.state }, State.Running) await nvim.command('bd!') }) it('should not start when client already started', async () => { await helper.edit('t.vim') const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, { documentSelector: [{ language: 'vim', scheme: 'file' }] }) await client.start() disposables.push(services.registerLanguageClient(client)) await nvim.command('bd!') }) }) describe('registerLanguageClient', () => { it('should not create client when not enabled', async () => { workspace.configurations.updateMemoryConfig({ languageserver: { test: { filetypes: ['vim'], enabled: false } } }) disposables.push(services.registerLanguageClient('test', { filetypes: ['vim'], enable: true })) let client = services.getService('test') expect(client).toBeDefined() await client.start() expect(client.state).toBe(ServiceStat.Initial) }) it('should not start for bad config', async () => { workspace.configurations.updateMemoryConfig({ languageserver: { test: { filetypes: ['vim'] } } }) disposables.push(services.registerLanguageClient('test', { filetypes: ['vim'], enable: true })) let client = services.getService('test') expect(client).toBeDefined() await client.start() expect(client.state).toBe(ServiceStat.Initial) }) it('should start and stop language client', async () => { let config = { filetypes: ['vim'], module: serverModule, enabled: false } workspace.configurations.updateMemoryConfig({ languageserver: { test: config } }) disposables.push(services.registerLanguageClient('test', config)) disposables.push(services.registerLanguageClient('test', config)) let client = services.getService('test') let p = client.start() void client.start() await p await client.start() await client.restart() let pro = client.stop() void client.stop() await pro expect(client.state).toBe(ServiceStat.Stopped) }) it('should start language client by restart', async () => { let config = { filetypes: ['vim'], module: serverModule, enabled: false } workspace.configurations.updateMemoryConfig({ languageserver: { test: config } }) disposables.push(services.registerLanguageClient('test', config)) let client = services.getService('test') await client.restart() expect(client.state).toBe(ServiceStat.Running) }) it('should not throw on start error', async () => { const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, {}) let spy = jest.spyOn(client, 'start').mockImplementation(() => { throw new Error('custom error') }) disposables.push(services.registerLanguageClient(client)) let service = services.getService('test') await service.start() spy.mockRestore() let line = await helper.getCmdline() expect(line).toMatch('failed to start') }) it('should sendRequest & sendNotification', async () => { const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, {}) disposables.push(services.registerLanguageClient(client)) let service = services.getService('test') await service.start() let res = await helper.plugin.cocAction('sendRequest', 'test', 'request', { value: 2 }) expect(res).toBe(3) await helper.plugin.cocAction('sendNotification', 'test', 'notification', {}) let result = await service.client.sendRequest('notified') expect(result).toEqual({ notified: true }) }) it('should throw when service not found', async () => { let fn = async () => { await services.sendNotification('id', 'method') } await expect(fn()).rejects.toThrow(Error) }) it('should register notification when client created', async () => { const serverOptions: ServerOptions = { module: serverModule, transport: TransportKind.ipc, } const client = new LanguageClient('test', 'Test Language Server', serverOptions, {}) services.registerLanguageClient(client) let service = services.getService('test') await helper.plugin.cocAction('registerNotification', 'test', 'notification') await service.start() await service.client.sendNotification('triggerNotification') await helper.wait(10) await services.stop('test') }) it('should register notification when client not created', async () => { await helper.plugin.cocAction('registerNotification', 'def', 'notification') workspace.configurations.updateMemoryConfig({ languageserver: { def: { filetypes: ['vim'], module: serverModule, } } }) services.registerLanguageClient('def', { filetypes: ['.vim'], module: serverModule }, URI.file(__dirname)) let res let spy = jest.spyOn(services, 'sendNotificationVim' as any).mockImplementation((id, method, result) => { res = { id, method, result } }) let service = services.getService('def') await service.start() await service.client.sendNotification('triggerNotification') await helper.waitValue(() => { return res != undefined }, true) await services.stop('def') spy.mockRestore() expect(res).toEqual({ id: 'def', method: 'notification', result: { x: 1 } }) }) }) }) ================================================ FILE: src/__tests__/modules/sources.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import path from 'path' import { Disposable } from 'vscode-languageserver-protocol' import sources from '../../completion/sources' import { ISource, SourceType } from '../../completion/types' import events from '../../events' import { disposeAll } from '../../util' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) }) describe('sources', () => { it('should check commit', () => { expect(sources.shouldCommit(undefined, undefined, '')).toBe(false) let source = sources.getSource('$words') expect(sources.shouldCommit(source, { word: '' }, '.')).toBe(false) expect(sources.shouldCommit(source, { word: '' }, '')).toBe(false) }) it('should get normal sources', () => { sources.createSource({ name: 'name', documentSelector: [{ language: 'vim' }], doComplete: () => null }) let arr = sources.getNormalSources('', 'test:///1') let res = arr.find(o => o.name === 'name') expect(res).toBeUndefined() sources.createSource({ name: 'name', documentSelector: [{ language: '*' }], doComplete: () => null }) arr = sources.getNormalSources('x', 'test:///1') res = arr.find(o => o.name === 'name') expect(res).toBeDefined() }) it('should get trigger sources', () => { let res = sources.getTriggerSources('', 'vim', 'test:///1') expect(res).toEqual([]) let arr = ['around', 'buffer', 'file'] res = sources.getTriggerSources('', 'vim', 'test:///1', arr) let find = res.find(o => arr.includes(o.name)) expect(find).toBeUndefined() sources.createSource({ name: 'name', documentSelector: [{ language: 'vim' }], doComplete: () => null }) helper.updateConfiguration('coc.source.name.triggerCharacters', ['.']) res = sources.getTriggerSources('.', 'vim', 'test:///1', arr) find = res.find(o => o.name === 'name') expect(find).toBeDefined() res = sources.getTriggerSources('.', 'txt', 'test:///1', arr) find = res.find(o => o.name === 'name') expect(find).toBeUndefined() }) it('should do document enter', async () => { let fn = jest.fn() let source: ISource = { name: 'enter', enable: true, priority: 0, sourceType: SourceType.Service, triggerCharacters: [], doComplete: () => Promise.resolve({ items: [] }), onEnter: fn } disposables.push(sources.addSource(source)) let buffer = await nvim.buffer await events.fire('BufEnter', [buffer.id]) expect(fn).toHaveBeenCalled() }) it('should get sources by split filetypes', async () => { disposables.push(sources.addSource({ name: 'foo', filetypes: ['foo'], enable: true, doComplete: () => Promise.resolve({ items: [] }), })) disposables.push(sources.addSource({ name: 'bar', filetypes: ['bar'], enable: true, doComplete: () => Promise.resolve({ items: [] }), })) let arr = sources.getNormalSources('foo.bar', 'file:///a') let names = arr.map(s => s.name) expect(names.includes('foo')).toBe(true) expect(names.includes('bar')).toBe(true) }) it('should return source states', async () => { disposables.push(sources.addSource({ name: 'foo', documentSelector: ['vim'], enable: true, doComplete: () => Promise.resolve({ items: [] }), })) let stats = await helper.doAction('sourceStat') expect(stats.length > 1).toBe(true) }) it('should toggle source state', async () => { await helper.doAction('toggleSource', 'around') let s = sources.getSource('around') expect(s.enable).toBe(false) sources.toggleSource('around') }) }) describe('sources#has', () => { it('should has source', () => { expect(sources.has('around')).toBe(true) }) it('should not has source', () => { expect(sources.has('NotExists')).toBe(false) }) }) describe('sources#refresh', () => { it('should refresh if possible', async () => { let fn = jest.fn() let source: ISource = { name: 'refresh', enable: true, priority: 0, sourceType: SourceType.Service, triggerCharacters: [], doComplete: () => Promise.resolve({ items: [] }), refresh: fn } disposables.push(sources.addSource(source)) await helper.doAction('refreshSource', 'refresh') expect(fn).toHaveBeenCalled() }) it('should work if refresh not defined', async () => { let source: ISource = { name: 'refresh', enable: true, priority: 0, sourceType: SourceType.Service, triggerCharacters: [], doComplete: () => Promise.resolve({ items: [] }) } disposables.push(sources.addSource(source)) await sources.refresh('refresh') }) }) describe('sources#createSource', () => { it('should throw on create source', async () => { expect(() => { sources.createSource({ doComplete: () => Promise.resolve({ items: [{ word: 'custom' }] }) } as any) }).toThrow() }) it('should create vim source', async () => { let folder = path.resolve(__dirname, '..') await nvim.command(`set runtimepath+=${folder}`) disposables.push({ dispose: () => { sources.removeSource('email') } }) await helper.waitValue(() => { return sources.has('email') }, true) await helper.createDocument() await nvim.input('i@') await helper.visible('foo@gmail.com') }) }) describe('sources#getTriggerSources()', () => { it('should filter by filetypes', async () => { let source: ISource = { name: 'test', enable: true, priority: 0, filetypes: ['javascript'], sourceType: SourceType.Service, triggerCharacters: ['#'], doComplete: () => Promise.resolve({ items: [] }) } disposables.push(sources.addSource(source)) let res = sources.getTriggerSources('#', 'javascript', 'file:///tmp.js') expect(res.find(o => o.name == 'test')).toBeDefined() }) it('should filter by documentSelector', async () => { let source: ISource = { name: 'test', enable: true, priority: 0, documentSelector: [{ language: 'javascript' }], sourceType: SourceType.Service, triggerCharacters: ['#'], doComplete: () => Promise.resolve({ items: [] }) } disposables.push(sources.addSource(source)) let res = sources.getTriggerSources('#', 'javascript', 'file:///tmp.js') expect(res.find(o => o.name == 'test')).toBeDefined() }) it('should filter disabled sources', async () => { await nvim.setLine('foo bar ') let buf = await nvim.buffer await buf.setVar('coc_disabled_sources', ['around', 'buffer', 'file']) await nvim.input('Af') await helper.wait(30) await nvim.input('/') await helper.wait(100) let visible = await nvim.call('pumvisible') expect(visible).toBe(0) }) }) ================================================ FILE: src/__tests__/modules/strWidth.test.ts ================================================ import { initStrWidthWasm, StrWidth, StrWidthWasi } from '../../model/strwidth' let api: StrWidthWasi beforeAll(async () => { api = await initStrWidthWasm() }) describe('strWidth', () => { it('should get display width', async () => { let sw = new StrWidth(api) sw.setAmbw(true) expect(sw.getWidth('')).toBe(0) expect(sw.getWidth('foo')).toBe(3) expect(sw.getWidth('嘻嘻')).toBe(4) }) it('should slice when content too long', async () => { let sw = new StrWidth(api) expect(sw.getWidth('p'.repeat(8192))).toBe(4095) }) it('should use cache', async () => { let sw = new StrWidth(api) expect(sw.getWidth(' ', true)).toBe(1) expect(sw.getWidth(' ', true)).toBe(1) expect(sw.getWidth(' ', true)).toBe(1) }) }) ================================================ FILE: src/__tests__/modules/task.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterEach(() => { disposeAll(disposables) }) afterAll(async () => { await helper.shutdown() }) describe('task test', () => { it('should start task', async () => { let task = workspace.createTask('sleep') disposables.push(task) let started = await task.start({ cmd: 'sleep', args: ['50'] }) expect(started).toBe(true) }) it('should stop task', async () => { let task = workspace.createTask('sleep') disposables.push(task) await task.start({ cmd: 'sleep', args: ['50'] }) await task.stop() let running = await task.running expect(running).toBe(false) }) it('should emit exit event', async () => { let fn = jest.fn() let task = workspace.createTask('sleep') disposables.push(task) task.onExit(fn) await task.start({ cmd: 'sleep', args: ['50'] }) await helper.wait(10) await task.stop() expect(fn).toHaveBeenCalled() }) it('should emit stdout event', async () => { let file = await createTmpFile('echo foo') let task = workspace.createTask('echo') disposables.push(task) let p = new Promise(resolve => { let lines: string[] = [] task.onStdout(stdout => { lines.push(...stdout) }) task.onExit(() => { resolve(lines) }) }) await task.start({ cmd: '/bin/sh', args: [file] }) let lines = await p expect(lines).toEqual(['foo']) }) it('should change environment variables', async () => { let file = await createTmpFile('echo $NODE_ENV\necho $COC_NVIM_TEST') let task = workspace.createTask('ENV') disposables.push(task) let lines: string[] = [] task.onStdout(arr => { lines.push(...arr) }) let p = new Promise(resolve => { task.onExit(() => { resolve() }) }) await task.start({ cmd: '/bin/sh', args: [file], env: { NODE_ENV: 'production', COC_NVIM_TEST: 'yes' } }) await p expect(lines).toEqual(['production', 'yes']) let res = await nvim.call('getenv', 'COC_NVIM_TEST') expect(res).toBeNull() }) it('should receive stdout lines as expected', async () => { let file = await createTmpFile('echo 3\necho ""\necho 4') let task = workspace.createTask('ENV') let p = new Promise(resolve => { let lines: string[] = [] task.onStdout(arr => { lines.push(...arr) }) task.onExit(() => { resolve(lines) }) }) await task.start({ cmd: '/bin/sh', args: [file] }) let lines = await p expect(lines).toEqual(['3', '', '4']) task.dispose() }) it('should emit stderr event', async () => { let file = await createTmpFile('console.error("start\\n\\nend");') let task = workspace.createTask('error') disposables.push(task) let p = new Promise(resolve => { let lines: string[] = [] task.onStderr(arr => { lines.push(...arr) }) task.onExit(() => { resolve(lines) }) }) await task.start({ cmd: 'node', args: [file] }) let lines = await p expect(lines).toEqual(['start', '', 'end']) }) it('should not receive event from other task', async () => { let task1 = workspace.createTask('one') disposables.push(task1) let count = 0 let cb = () => { count++ } task1.onExit(cb) task1.onStderr(cb) task1.onStdout(cb) let file = await createTmpFile('console.log("start");console.error("end");') let task = workspace.createTask('error') await task.start({ cmd: 'node', args: [file] }) let promise = new Promise(resolve => { task.onExit(() => { resolve(undefined) }) }) await promise expect(count).toBe(0) }) }) ================================================ FILE: src/__tests__/modules/terminal.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import helper from '../helper' import { TerminalModel } from '../../model/terminal' let nvim: Neovim let terminal: TerminalModel beforeAll(async () => { await helper.setup() nvim = helper.nvim terminal = new TerminalModel('sh', [], nvim) await terminal.start(__dirname, { COC_TERMINAL: `option '-term'` }) }) afterAll(async () => { terminal.dispose() await helper.shutdown() }) describe('terminal properties', () => { it('should get name', () => { let name = terminal.name expect(name).toBe('sh') }) it('should have correct cwd and env', async () => { let bufnr = terminal.bufnr terminal.sendText('echo $PWD') await helper.wait(300) let lines = await nvim.call('getbufline', [bufnr, 1, '$']) as string[] expect(lines[0].trim().length).toBeGreaterThan(0) terminal.sendText('echo $COC_TERMINAL') await helper.wait(300) lines = await nvim.call('getbufline', [bufnr, 1, '$']) as string[] expect(lines.includes(`option '-term'`)).toBe(true) terminal.onExit(-1) }) it('should get pid', async () => { let pid = await terminal.processId expect(typeof pid).toBe('number') }) it('should hide terminal window', async () => { await terminal.hide() let winnr = await nvim.call('bufwinnr', terminal.bufnr) expect(winnr).toBe(-1) }) it('should show terminal window', async () => { await terminal.show() let winnr = await nvim.call('bufwinnr', terminal.bufnr) expect(winnr != -1).toBe(true) }) it('should not throw when not shown', async () => { let terminal = new TerminalModel('sh', [], nvim) terminal.sendText('text') await terminal.start(__dirname, {}) await terminal.show() await terminal.show() }) }) ================================================ FILE: src/__tests__/modules/util.test.ts ================================================ import style from 'ansi-styles' import * as assert from 'assert' import cp, { spawn } from 'child_process' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import vm from 'vm' import { AnnotatedTextEdit, CancellationToken, CancellationTokenSource, ChangeAnnotation, Color, Position, Range, SymbolKind, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol' import { ConfigurationScope } from '../../configuration/types' import { LinesTextDocument } from '../../model/textdocument' import { DocumentChange } from '../../types' import { concurrent, delay, disposeAll, wait, waitWithToken } from '../../util' import { ansiparse, parseAnsiHighlights } from '../../util/ansiparse' import * as arrays from '../../util/array' import { filter, forEach, map, YieldOptions } from '../../util/async' import * as color from '../../util/color' import { pluginRoot } from '../../util/constants' import { getSymbolKind } from '../../util/convert' import * as diff from '../../util/diff' import * as errors from '../../util/errors' import * as extension from '../../util/extensionRegistry' import * as factory from '../../util/factory' import * as fuzzy from '../../util/fuzzy' import * as Is from '../../util/is' import { Extensions, IJSONContributionRegistry } from '../../util/jsonRegistry' import * as lodash from '../../util/lodash' import { Mutex } from '../../util/mutex' import * as numbers from '../../util/numbers' import * as objects from '../../util/object' import * as platform from '../../util/platform' import * as positions from '../../util/position' import { executable, isRunning, runCommand, terminate } from '../../util/processes' import { convertProperties, Registry } from '../../util/registry' import { Sequence } from '../../util/sequence' import * as strings from '../../util/string' import * as textedits from '../../util/textedit' import { createTiming } from '../../util/timing' import helper from '../helper' function createTextDocument(lines: string[]): LinesTextDocument { return new LinesTextDocument('file://a', 'txt', 1, lines, 1, true) } function toEdit(sl, sc, el, ec, text): TextEdit { return TextEdit.replace(Range.create(sl, sc, el, ec), text) } let logfile = path.join(os.tmpdir(), 'log_test.js') beforeAll(() => { let code = `const {wait, nvim} = require('coc.nvim') console.log('log') console.debug('debug') console.info('info') console.error('error') console.warn('warn') module.exports = () => { return {wait, nvim} }` fs.writeFileSync(logfile, code, 'utf8') }) afterAll(() => { fs.unlinkSync(logfile) }) describe('factory', () => { afterAll(() => { global.__TEST__ = true }) const emptyLogger: factory.ILogger = { log: () => {}, info: () => {}, error: () => {}, debug: () => {}, warn: () => {}, trace: () => {}, fatal: () => {}, mark: () => {} } it('should create logger', () => { let fn = jest.fn() const sandbox = factory.createSandbox(logfile, { log: () => { fn() }, info: () => { fn() }, error: () => { fn() }, debug: () => { fn() }, warn: () => { fn() }, trace: () => { }, fatal: () => { }, mark: () => { } }) vm.runInContext(` console.log('log') console.debug('debug') console.info('info') console.error('error') console.warn('warn')`, sandbox) expect(fn).toHaveBeenCalled() }) it('should create console', () => { let res = factory.createConsole({ x: 1 }, {} as any) expect(res).toEqual({ x: 1 }) let called = false let val = 1 res = factory.createConsole({ warn: () => { }, custom: () => { val = 2 } }, { warn: () => { called = true } } as any) ; (res as any).custom() ; (res as Console).warn() expect(val).toBe(1) expect(called).toBe(true) }) it('should copy properties', () => { let obj = factory.copyGlobalProperties({} as any, global) expect(typeof obj['fetch']).toBe('function') }) it('should not throw process.chdir', () => { const sandbox = factory.createSandbox(logfile, emptyLogger) let res = vm.runInContext(`process.chdir()`, sandbox) expect(res).toBeUndefined() }) it('should throw with umask', () => { const sandbox = factory.createSandbox(logfile, emptyLogger) let res = vm.runInContext(`process.umask()`, sandbox) expect(typeof res).toBe('number') let err try { res = vm.runInContext(`process.umask(18)`, sandbox) } catch (e) { err = e } expect(err).toBeDefined() }) it('should throw with process.exit', () => { const sandbox = factory.createSandbox(logfile, emptyLogger) let err try { vm.runInContext(`process.exit()`, sandbox) } catch (e) { err = e } expect(err).toBeDefined() }) it('should get module prototype', () => { const Module = require('module') expect(factory.getProtoWithCompile(Module as any)).toBeDefined() function fn() {} expect(() => { factory.getProtoWithCompile(fn) }).toThrow(Error) fn.prototype._compile = () => {} expect(factory.getProtoWithCompile(fn)).toBeDefined() }) it('should hook require', () => { const sandbox = factory.createSandbox(logfile, factory.consoleLogger, 'hook', false) let fn = factory.compileInSandbox(sandbox, { wait() {} }) let obj: any = {} fn.apply(obj, [`const {wait} = require('coc.nvim')\nmodule.exports = wait`, logfile]) expect(typeof obj.exports).toBe('function') }) it('should createSandbox', () => { const Module = require('module') const sandbox = factory.createSandbox(logfile, emptyLogger, 'hook', false) delete Module._cache[require.resolve(logfile)] let exports = sandbox.require(logfile) expect(typeof exports).toBe('function') let obj = exports() expect(typeof obj.wait).toBe('function') }) it('should clear the cache', () => { const Module = require('module') let filename = path.join(os.tmpdir(), 'cache_test.js') fs.writeFileSync(filename, 'module.exports = {x: 1}', 'utf8') let sandbox = factory.createSandbox(filename, emptyLogger, 'hook') let exports = sandbox.require(filename) delete Module._cache[require.resolve(filename)] fs.writeFileSync(filename, 'module.exports = {y: 1}', 'utf8') sandbox = factory.createSandbox(filename, emptyLogger, 'hook') exports = sandbox.require(filename) expect(exports).toEqual({ y: 1 }) fs.rmSync(filename, { force: true }) }) it('should create extension', () => { global.__TEST__ = false let filename = path.join(os.tmpdir(), 'hash.js') fs.writeFileSync(filename, `#! /usr/bin/env node module.exports = function(){ return {fs: require("fs"), resolved: require.resolve('fs')} }`, 'utf8') let exp = factory.createExtension('hash', filename, false) as any let res = exp.activate() expect(res.fs).toBeDefined() expect(res.resolved).toBe('fs') fs.rmSync(filename, { force: true }) }) }) describe('platform', () => { it('should get platform', () => { expect(platform.getPlatform({ platform: 'win32' } as any)).toBe(platform.Platform.Windows) expect(platform.getPlatform({ platform: 'darwin' } as any)).toBe(platform.Platform.Mac) expect(platform.getPlatform({ platform: 'linux' } as any)).toBe(platform.Platform.Linux) expect(platform.getPlatform({ platform: 'unknown' } as any)).toBe(platform.Platform.Unknown) }) it('should check platform', () => { expect(platform.isWeb).toBeDefined() expect(platform.isLinux).toBeDefined() expect(platform.isNative).toBeDefined() expect(platform.isWindows).toBeDefined() expect(platform.isMacintosh).toBeDefined() }) }) describe('textedit', () => { function createEdit(uri: string): WorkspaceEdit { let edit = TextEdit.insert(Position.create(0, 0), 'a') let doc = { uri, version: null } return { documentChanges: [TextDocumentEdit.create(doc, [edit])] } } function addPosition(position: Position, line: number, character: number): Position { return Position.create(position.line + line, position.character + character) } test('getChangedPosition', () => { const assertPosition = (start, edit, arr) => { let res = textedits.getChangedPosition(start, edit) expect(res).toEqual(Position.create(arr[0], arr[1])) } let pos = Position.create(0, 0) assertPosition(pos, TextEdit.insert(pos, 'abc'), [0, 3]) assertPosition(pos, TextEdit.insert(pos, 'a\nb\nc'), [2, 1]) let edit = TextEdit.replace(Range.create(pos, Position.create(0, 3)), 'abc') assertPosition(pos, edit, [0, 0]) pos = Position.create(0, 1) let r = Range.create(addPosition(pos, 0, -1), pos) assertPosition(pos, TextEdit.replace(r, 'a\nb\n'), [2, -1]) pos = Position.create(1, 3) edit = TextEdit.replace(Range.create(Position.create(0, 1), Position.create(1, 0)), 'abc') assertPosition(pos, edit, [-1, 4]) }) test('getChangedLineCount', () => { let pos = Position.create(5, 0) let edits: TextEdit[] = [ TextEdit.replace(Range.create(0, 1, 1, 0), ''), TextEdit.replace(Range.create(2, 1, 3, 0), ''), TextEdit.replace(Range.create(10, 1, 12, 0), 'foo'), ] expect(textedits.getChangedLineCount(pos, edits)).toBe(-2) }) test('getPosition()', () => { let pos = Position.create(1, 3) const assertChange = (rl, rc, el, ec, text, val): void => { let edit = TextEdit.replace(Range.create(rl, rc, el, ec), text) let res = textedits.getPosition(pos, edit) expect(res).toEqual(val) } assertChange(0, 1, 1, 0, 'abc', Position.create(0, 7)) assertChange(0, 1, 1, 1, 'abc', Position.create(0, 6)) assertChange(0, 1, 1, 0, 'abc\n', Position.create(1, 3)) assertChange(1, 1, 1, 2, '', Position.create(1, 2)) assertChange(1, 1, 3, 0, '', Position.create(1, 3)) }) test('getStartLine()', () => { const assertLine = (rl, rc, el, ec, text, val: number): void => { let edit = TextEdit.replace(Range.create(rl, rc, el, ec), text) let res = textedits.getStartLine(edit) expect(res).toBe(val) } assertLine(0, 0, 0, 0, 'abc\n', -1) assertLine(1, 0, 1, 0, 'd\n', 0) assertLine(0, 0, 0, 0, 'abc', 0) }) test('getPositionFromEdits()', () => { const assertEdits = (pos, edits, exp: [number, number]) => { let res = textedits.getPositionFromEdits(pos, edits) expect(res).toEqual(Position.create(exp[0], exp[1])) } let pos = Position.create(5, 1) let edits: TextEdit[] = [ TextEdit.replace(Range.create(0, 3, 1, 0), ''), TextEdit.replace(Range.create(2, 4, 3, 0), ''), TextEdit.replace(Range.create(3, 4, 4, 0), ''), TextEdit.replace(Range.create(4, 1, 5, 0), ''), TextEdit.replace(Range.create(6, 1, 6, 1), 'foo'), ] assertEdits(pos, edits, [1, 10]) }) it('should check empty workspaceEdit', () => { let workspaceEdit: WorkspaceEdit = createEdit('untitled:/1') expect(textedits.emptyWorkspaceEdit(workspaceEdit)).toBe(false) expect(textedits.emptyWorkspaceEdit({ documentChanges: [] })).toBe(true) }) it('should get ranges', async () => { let ranges = textedits.getRangesFromEdit('test:/1', {}) expect(ranges).toBeUndefined() let edit: WorkspaceEdit = { changes: { 'test:/2': [TextEdit.insert(Position.create(0, 0), ' ')] } } ranges = textedits.getRangesFromEdit('test:/1', edit) expect(ranges).toBeUndefined() ranges = textedits.getRangesFromEdit('test:/2', edit) expect(ranges).toBeDefined() edit = { documentChanges: [TextDocumentEdit.create({ uri: 'test:/1', version: null }, [TextEdit.insert(Position.create(0, 0), ' ')])] } ranges = textedits.getRangesFromEdit('test:/1', edit) expect(ranges).toBeDefined() }) it('should get all annotation ids for confirm', () => { let doc = { uri: 'test:///1', version: null } let changes: DocumentChange[] = [] let ids = [uuid(), uuid(), uuid()] changes.push({ textDocument: doc, edits: [ AnnotatedTextEdit.insert(Position.create(0, 0), 'foo', ids[0]), AnnotatedTextEdit.insert(Position.create(1, 0), 'bar', ids[1]), ] }) changes.push({ kind: 'delete', uri: 'test:///2', annotationId: ids[2] }) changes.push({ kind: 'delete', uri: 'test:///3', }) let annotations: { [id: string]: ChangeAnnotation } = {} annotations[ids[0]] = { label: '0', needsConfirmation: true } annotations[ids[1]] = { label: '1', needsConfirmation: true } annotations[ids[2]] = { label: '2', needsConfirmation: true } let res = textedits.getConfirmAnnotations(changes, annotations) expect(res.length).toBe(3) }) it('should create filtered changes', () => { let doc = { uri: 'test:///1', version: null } let changes: DocumentChange[] = [] let ids = [uuid(), uuid(), uuid()] changes.push({ textDocument: doc, edits: [ AnnotatedTextEdit.insert(Position.create(0, 0), 'foo', ids[0]), AnnotatedTextEdit.insert(Position.create(1, 0), 'bar', ids[1]), ] }) changes.push({ kind: 'delete', uri: 'test:///2', annotationId: ids[2] }) changes.push({ kind: 'delete', uri: 'test:///3', }) let res = textedits.createFilteredChanges(changes, [ids[0], ids[2]]) expect(res.length).toBe(2) expect(res).toEqual([{ textDocument: { uri: "test:///1", version: null }, edits: [{ range: { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } }, newText: "bar", annotationId: ids[1] }] }, { kind: "delete", uri: "test:///3" }]) res = textedits.createFilteredChanges(changes, ids) expect(res.length).toBe(1) }) it('should check edit is denied', () => { let ids = [uuid(), uuid()] let edits = [ AnnotatedTextEdit.insert(Position.create(0, 0), 'foo', ids[0]), AnnotatedTextEdit.insert(Position.create(1, 0), 'bar', ids[1]), ] expect(textedits.isDeniedEdit(edits[0], [ids[0]])).toBe(true) expect(textedits.isDeniedEdit(edits[1], [ids[0]])).toBe(false) }) it('should check empty TextEdit', () => { expect(textedits.emptyTextEdit(TextEdit.insert(Position.create(0, 0), ''))).toBe(true) expect(textedits.emptyTextEdit(TextEdit.insert(Position.create(0, 0), 'a'))).toBe(false) }) it('should get well formed edit', () => { let r = Range.create(1, 0, 0, 0) let edit: TextEdit = { range: r, newText: 'foo' } let res = textedits.getWellformedEdit(edit) expect(res.range).toEqual(Range.create(0, 0, 1, 0)) r = Range.create(0, 0, 1, 0) edit = { range: r, newText: 'foo' } res = textedits.getWellformedEdit(edit) expect(res.range).toBe(r) }) it('should check line count change', () => { let r = Range.create(0, 0, 0, 5) let edit: TextEdit = { range: r, newText: 'foo' } expect(textedits.lineCountChange(edit)).toBe(0) edit = { range: Range.create(0, 0, 1, 0), newText: 'foo' } expect(textedits.lineCountChange(edit)).toBe(-1) }) it('should filter and sort textedits', () => { let doc = createTextDocument(['foo']) expect(textedits.filterSortEdits(doc, [TextEdit.insert(Position.create(0, 0), 'a\r\nb')])).toEqual([ TextEdit.insert(Position.create(0, 0), 'a\nb') ]) expect(textedits.filterSortEdits(doc, [TextEdit.replace(Range.create(0, 0, 0, 3), 'foo')])).toEqual([]) expect(textedits.filterSortEdits(doc, [ TextEdit.insert(Position.create(0, 1), 'b'), TextEdit.insert(Position.create(0, 0), 'a'), ])).toEqual([ TextEdit.insert(Position.create(0, 0), 'a'), TextEdit.insert(Position.create(0, 1), 'b'), ]) }) it('should fix edit range', () => { let doc = createTextDocument(['foo']) let range = Range.create(0, 0, 0, 5) let res = textedits.filterSortEdits(doc, [TextEdit.replace(range, 'bar')]) expect(res[0].range).toEqual(Range.create(0, 0, 0, 3)) }) it('should get range text', async () => { { let text = textedits.getRangeText([''], Range.create(0, 0, 0, 0)) expect(text).toBe('') } { let lines = ['foo', 'aabb', 'bar'] let text = textedits.getRangeText(lines, Range.create(0, 1, 2, 1)) expect(text).toBe('oo\naabb\nb') } }) it('should reduceTextEdit', () => { let e: TextEdit e = TextEdit.replace(Range.create(0, 0, 0, 3), 'foo') expect(textedits.reduceTextEdit(e, '')).toEqual(e) e = TextEdit.replace(Range.create(0, 0, 0, 3), 'foo\nbar') expect(textedits.reduceTextEdit(e, 'bar')).toEqual( TextEdit.replace(Range.create(0, 0, 0, 0), 'foo\n') ) e = TextEdit.replace(Range.create(0, 0, 0, 3), 'foo\nbar') expect(textedits.reduceTextEdit(e, 'foo')).toEqual( TextEdit.replace(Range.create(0, 3, 0, 3), '\nbar') ) e = TextEdit.replace(Range.create(0, 0, 0, 3), 'def') expect(textedits.reduceTextEdit(e, 'daf')).toEqual( TextEdit.replace(Range.create(0, 1, 0, 2), 'e') ) e = TextEdit.replace(Range.create(2, 0, 3, 0), 'ascii ascii bar\n') expect(textedits.reduceTextEdit(e, 'xyz ascii bar\n')).toEqual( TextEdit.replace(Range.create(2, 0, 2, 3), 'ascii') ) }) it('should get revert edit', async () => { { let res = textedits.getRevertEdit(['aa'], ['aa'], 0) expect(res).toBeUndefined() } { let res = textedits.getRevertEdit(['foo', 'bar'], ['foo 1', 'bar 2'], 0) expect(res).toEqual(TextEdit.replace(Range.create(0, 0, 2, 0), 'foo\nbar\n')) } { let res = textedits.getRevertEdit(['foo', 'bar'], ['foo', 'bar', 'after'], 2) expect(res).toEqual(TextEdit.replace(Range.create(2, 0, 3, 0), '')) } }) it('should merge textedits #1', () => { let edits = [toEdit(0, 0, 0, 0, 'foo'), toEdit(0, 1, 0, 1, 'bar')] let lines = ['ab'] let res = textedits.mergeTextEdits(edits, lines, ['fooabarb']) expect(res).toEqual(toEdit(0, 0, 0, 1, 'fooabar')) }) it('should merge textedits #2', () => { let edits = [toEdit(0, 0, 1, 0, 'foo\n')] let lines = ['bar'] let res = textedits.mergeTextEdits(edits, lines, ['foo']) expect(res).toEqual(toEdit(0, 0, 1, 0, 'foo\n')) }) it('should merge textedits #3', () => { let edits = [toEdit(0, 0, 0, 1, 'd'), toEdit(1, 0, 1, 1, 'e'), toEdit(2, 0, 3, 0, 'f\n')] let lines = ['a', 'b', 'c'] let res = textedits.mergeTextEdits(edits, lines, ['d', 'e', 'f']) expect(res).toEqual(toEdit(0, 0, 3, 0, 'd\ne\nf\n')) }) it('should convert to text changes', () => { expect(textedits.validEdit(TextEdit.insert(Position.create(0, 0), 'abc'))).toBe(false) expect(textedits.validEdit(TextEdit.insert(Position.create(0, 1), 'abc\n'))).toBe(false) expect(textedits.toTextChanges(['foo'], [])).toEqual([]) expect(textedits.toTextChanges(['foo'], [TextEdit.insert(Position.create(3, 1), '')])).toEqual([]) expect(textedits.toTextChanges(['foo'], [TextEdit.insert(Position.create(1, 1), '')])).toEqual([]) expect(textedits.toTextChanges(['foo'], [TextEdit.insert(Position.create(1, 0), 'bar\n')])).toEqual([[['', 'bar'], 0, 3, 0, 3]]) expect(textedits.toTextChanges(['foo'], [TextEdit.replace(Range.create(0, 0, 1, 0), 'bar\n')])).toEqual([[['bar'], 0, 0, 0, 3]]) }) }) describe('Registry', () => { it('should add to registry', () => { Registry.add('key', {}) expect(Registry.knows('key')).toBe(true) expect(Registry.as('key')).toEqual({}) expect(Registry.as('not_exists')).toBeNull() }) it('should get jsonRegistry', () => { let r = Registry.as(Extensions.JSONContribution) expect(r).toBeDefined() r.registerSchema('uri', {} as any) let res = r.getSchemaContributions() expect(res.schemas.uri).toBeDefined() }) it('should convertProperties', () => { expect(convertProperties(undefined)).toEqual({}) expect(convertProperties({ key: { type: 'number' } }, ConfigurationScope.RESOURCE)).toEqual({ key: { scope: ConfigurationScope.RESOURCE, type: 'number' } }) let properties = { foo: { }, bar: { type: 'string', scope: 'language-overridable' }, resource: { type: 'string', scope: 'resource' }, window: { type: 'string', default: '' }, format: { type: 'string', scope: 'window' }, 'coc.source.name': { type: 'string', scope: 'resource' }, 'list.source.name': { type: 'string', scope: 'resource' }, } let res = convertProperties(properties) expect(res.foo).toBeDefined() expect(res.format.scope).toBe(ConfigurationScope.WINDOW) expect(res.bar.scope).toBe(ConfigurationScope.LANGUAGE_OVERRIDABLE) expect(res.resource.scope).toBe(ConfigurationScope.RESOURCE) expect(res.window.scope).toBe(ConfigurationScope.WINDOW) expect(res['coc.source.name'].scope).toBe(ConfigurationScope.APPLICATION) expect(res['list.source.name'].scope).toBe(ConfigurationScope.APPLICATION) }) it('should parse extension name', () => { let parseSource = extension.parseExtensionName expect(parseSource(``)).toBeUndefined() expect(parseSource(`a)`, 0)).toBe('coc.nvim') expect(parseSource(`a`, 0)).toBe('coc.nvim') let registry = Registry.as(extension.Extensions.ExtensionContribution) let filepath = path.join(os.tmpdir(), 'single') registry.registerExtension('single', { name: 'single', directory: os.tmpdir(), filepath }) expect(parseSource(`\n\n${filepath}:1:1`)).toBe('single') // expect(parseSource(`\n\n${filepath.slice(0, -3)}:1:1`)).toBeUndefined() expect(parseSource(`\n\n/a/b:1:1`)).toBe('coc.nvim') let dir = fs.realpathSync(os.tmpdir()) expect(parseSource(`\n\n${path.join(dir, 'foo')}:1:1`)).toBe('single') let lines = [ `at FormatRangeManager.addProvider (${pluginRoot}/src/provider/manager.ts:28:55`, `at FormatRangeManager.register (${pluginRoot}/formatRangeManager.ts:17:17)`, `at PrettierEditService.registerDocumentFormatEditorProviders (${filepath}:253:17)` ] let res = parseSource(`\n\n${lines.join('\n')}`, 2) expect(res).toBe('single') registry.unregistExtension('single') }) it('should check rootPattern and commands', () => { expect(extension.validRootPattern({} as any)).toBe(false) expect(extension.validCommandContribution({} as any)).toBe(false) }) it('should get properties', () => { let properties = extension.getProperties({}) expect(properties).toEqual({}) properties = extension.getProperties({ properties: { x: 1 } }) expect(properties).toEqual({ x: 1 }) properties = extension.getProperties([{ properties: { x: 1 } }, { properties: { y: 2 } }]) expect(properties).toEqual({ x: 1, y: 2 }) }) it('should get onCommands and commands', () => { let registry = Registry.as(extension.Extensions.ExtensionContribution) registry.registerExtension('single', { name: 'single', directory: os.tmpdir(), onCommands: ['a', 'b', 'cmd', undefined], commands: [{ command: 'cmd', title: 'title' }] }) expect(registry.commands.length).toBeGreaterThan(0) expect(registry.onCommands.length).toBeGreaterThan(0) expect(registry.getCommandTitle('cmd')).toBe('title') expect(registry.getCommandTitle('not_exists')).toBeUndefined() registry.unregistExtension('single') }) it('should get rootPatterns by fieltype', () => { let registry = Registry.as(extension.Extensions.ExtensionContribution) registry.registerExtension('single', { name: 'single', directory: os.tmpdir(), rootPatterns: [{ filetype: 'vim', patterns: ['.foo', '.bar', undefined] }] }) expect(registry.getRootPatternsByFiletype('vim')).toEqual(['.foo', '.bar']) expect(registry.getRootPatternsByFiletype('ts')).toEqual([]) registry.unregistExtension('single') }) }) describe('errors', () => { it('should return errors', () => { expect(errors.directoryNotExists('dir').message).toMatch('dir') expect(errors.illegalArgument('name') instanceof Error).toBe(true) expect(errors.illegalArgument() instanceof Error).toBe(true) expect(errors.shouldNotAsync('method') instanceof Error).toBe(true) errors.onUnexpectedError(new errors.CancellationError()) expect(() => { errors.onUnexpectedError(new Error('my error')) }).toThrow() expect(() => { errors.onUnexpectedError('error') }).toThrow() errors.assert(true) expect(() => { errors.assert(false) }).toThrow() }) it('should check CancellationError', () => { let err = new Error('Canceled') err.name = 'Canceled' expect(errors.isCancellationError(err)).toBe(true) expect(errors.shouldIgnore(err)).toBe(true) }) it('should check shouldIgnore', async () => { expect(errors.shouldIgnore(new errors.CancellationError())).toBe(true) let err = new Error('transport disconnected') expect(errors.shouldIgnore(err)).toBe(true) }) }) describe('numbers', () => { it('should work with numbers', () => { expect(numbers.toNumber(undefined, 5)).toBe(5) expect(numbers.toNumber(undefined)).toBe(0) expect(numbers.toNumber(1, 5)).toBe(1) expect(numbers.clamp(1, 1, 3)).toBe(1) expect(numbers.clamp(5, 1, 3)).toBe(3) expect(numbers.rot(6, 5)).toBe(1) }) }) describe('strings', () => { it('should get byte indexes', () => { let bytes = strings.bytes let fn = bytes('abcde') expect(fn(0)).toBe(0) expect(fn(1)).toBe(1) expect(fn(8)).toBe(5) fn = bytes('你ab好') expect(fn(0)).toBe(0) expect(fn(1)).toBe(3) expect(fn(2)).toBe(4) fn = bytes('abcdefghi', 3) expect(fn(5)).toBe(3) fn = bytes('😘😘') expect(fn(2)).toBe(4) expect(fn(4)).toBe(8) fn = bytes(String.fromCharCode(0xdc02) + 'ab') expect(fn(2)).toBe(4) }) it('should get byte index from utf16 index', () => { let testIndex = (text: string, index: number) => { let res = Buffer.byteLength(text.slice(0, index)) expect(strings.byteIndex(text, index)).toBe(res) } testIndex('abc', 2) testIndex('汉字abc', 2) testIndex('汉字abc', 4) testIndex('😘foo', 3) testIndex('', 3) testIndex(String.fromCharCode(0xdc02) + 'ab', 2) }) it('should get byte length', () => { expect(strings.byteLength('a')).toBe(1) expect(strings.byteLength('你')).toBe(3) expect(strings.byteLength('a😘b')).toBe(6) expect(strings.byteLength('a😘b', 1)).toBe(5) expect(strings.byteLength('a😘b', 3)).toBe(1) }) it('should get character index from byte index', () => { expect(strings.characterIndex('ab', 0)).toBe(0) expect(strings.characterIndex('abc', 1)).toBe(1) expect(strings.characterIndex('ab', 99)).toBe(2) expect(strings.characterIndex('abc', 1)).toBe(1) expect(strings.characterIndex('ôbc', 2)).toBe(1) expect(strings.characterIndex('ô你c', 2)).toBe(1) expect(strings.characterIndex('你c', 3)).toBe(1) expect(strings.characterIndex('😘def', 4)).toBe(2) expect(strings.characterIndex('\ude18def', 3)).toBe(1) expect(strings.utf8_code2len(65537)).toBe(4) }) it('should slice content by bytes', () => { expect(strings.byteSlice('你', 0, 1)).toBe('你') expect(strings.byteSlice('你', 0, 3)).toBe('你') expect(strings.byteSlice('abc你', 3, 6)).toBe('你') expect(strings.byteSlice('foo', 1)).toBe('oo') }) it('should get case', () => { expect(strings.getCase('a'.charCodeAt(0))).toBe(1) expect(strings.getCase('A'.charCodeAt(0))).toBe(2) expect(strings.getCase('#'.charCodeAt(0))).toBe(0) }) it('should get next word code', () => { function assertNext(text: string, index: number, res: [number, string] | undefined): void { let arr = res === undefined ? undefined : [res[0], res[1].charCodeAt(0)] let result = strings.getNextWord(fuzzy.getCharCodes(text), index) expect(result).toEqual(arr) } assertNext('abc', 0, [0, 'a']) assertNext('abc', 1, undefined) assertNext('abC', 1, [2, 'C']) }) it('should get character indexes', () => { expect(strings.getCharIndexes('abaca', 'a')).toEqual([0, 2, 4]) expect(strings.getCharIndexes('abd', 'f')).toEqual([]) }) it('should convert to lines', () => { expect(strings.contentToLines('foo', false)).toEqual(['foo']) expect(strings.contentToLines('foo\n', true)).toEqual(['foo']) }) it('should get smartcaseIndex', () => { expect(strings.smartcaseIndex('a', 'A')).toBe(0) expect(strings.smartcaseIndex('a', 'a')).toBe(0) expect(strings.smartcaseIndex('ab', 'a')).toBe(-1) expect(strings.smartcaseIndex('', 'a')).toBe(0) expect(strings.smartcaseIndex('ab', 'xaB')).toBe(1) expect(strings.smartcaseIndex('aA', 'aaA')).toBe(1) expect(strings.smartcaseIndex('aB', 'aaA')).toBe(-1) expect(strings.smartcaseIndex('AA', 'aaA')).toBe(-1) expect(strings.smartcaseIndex('aA', 'axdefA')).toBe(-1) expect(strings.smartcaseIndex('abC', 'aaBDefabC')).toBe(6) }) it('should convert to integer', () => { expect(strings.toErrorText('a')).toBe('a') expect(strings.toInteger('a')).toBeUndefined() expect(strings.toInteger('1')).toBe(1) }) it('should check highlight character', () => { expect(strings.isHighlightGroupCharCode('1'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('9'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('a'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('z'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('A'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('Z'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('.'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('_'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode('@'.charCodeAt(0))).toBe(true) expect(strings.isHighlightGroupCharCode(' '.charCodeAt(0))).toBe(false) }) it('should convert to text', () => { expect(strings.toText(undefined)).toBe('') expect(strings.toText(null)).toBe('') expect(strings.toText(3)).toBe('3') }) it('should check isEmojiImprecise', () => { expect(strings.isEmojiImprecise(999)).toBe(false) expect(strings.isEmojiImprecise(0x1F1E7)).toBe(true) expect(strings.isEmojiImprecise(8987)).toBe(true) expect(strings.isEmojiImprecise(128764)).toBe(true) expect(strings.isEmojiImprecise(129008)).toBe(true) expect(strings.isEmojiImprecise(129782)).toBe(true) expect(strings.isEmojiImprecise(129535)).toBe(true) }) it('should get parts', () => { let res = strings.rangeParts('foo bar', Range.create(0, 0, 0, 4)) expect(res).toEqual(['', 'bar']) res = strings.rangeParts('foo\nbar', Range.create(0, 1, 1, 1)) expect(res).toEqual(['f', 'ar']) res = strings.rangeParts('x\nfoo\nbar\ny', Range.create(0, 1, 2, 3)) expect(res).toEqual(['x', '\ny']) res = strings.rangeParts('foo\nbar\nx', Range.create(1, 0, 1, 1)) expect(res).toEqual(['foo\n', 'ar\nx']) res = strings.rangeParts('x\nfoo\nbar\ny', Range.create(0, 1, 1, 0)) expect(res).toEqual(['x', 'foo\nbar\ny']) }) it('should equalsIgnoreCase', () => { expect(strings.equalsIgnoreCase('', '')).toBe(true) expect(!strings.equalsIgnoreCase('', '1')).toBe(true) expect(!strings.equalsIgnoreCase('1', '')).toBe(true) expect(strings.equalsIgnoreCase('a', 'a')).toBe(true) expect(strings.equalsIgnoreCase('abc', 'Abc')).toBe(true) expect(strings.equalsIgnoreCase('abc', 'ABC')).toBe(true) expect(strings.equalsIgnoreCase('Höhenmeter', 'HÖhenmeter')).toBe(true) expect(strings.equalsIgnoreCase('ÖL', 'Öl')).toBe(true) }) it('should doEqualsIgnoreCase', () => { expect(strings.doEqualsIgnoreCase('a', undefined)).toBe(false) expect(strings.doEqualsIgnoreCase('a', 'b')).toBe(false) expect(strings.doEqualsIgnoreCase('你', '的')).toBe(false) }) it('should find index', () => { expect(strings.indexOf('a,b,c', ',', 2)).toBe(3) expect(strings.indexOf('a,b,c', ',', 1)).toBe(1) expect(strings.indexOf('a,b,c', 't')).toBe(-1) }) it('should upperFirst', () => { expect(strings.upperFirst('')).toBe('') expect(strings.upperFirst('abC')).toBe('AbC') expect(strings.upperFirst(undefined)).toBe('') }) it('should getUnicodeClass', () => { expect(strings.getUnicodeClass(null)).toBe('other') expect(strings.getUnicodeClass('')).toBe('other') expect(strings.getUnicodeClass('\0')).toBe('other') expect(strings.getUnicodeClass('\x1b')).toBe('punctuation') expect(strings.getUnicodeClass(',')).toBe('punctuation') expect(strings.getUnicodeClass('你')).toBe('cjkideograph') expect(strings.getUnicodeClass('😘')).toBe('other') expect(strings.getUnicodeClass('a')).toBe('word') }) }) describe('getSymbolKind()', () => { it('should get symbol kind', () => { for (let i = 1; i <= 27; i++) { expect(getSymbolKind(i as SymbolKind)).toBeDefined() } }) }) describe('Is', () => { it('should url', () => { expect(Is.isUrl('')).toBe(false) expect(Is.isUrl(undefined)).toBe(false) expect(Is.isUrl('file:1')).toBe(true) }) it('should check insert replace edit', () => { expect(Is.isEditRange(null)).toBe(false) let r = Range.create(0, 0, 0, 1) expect(Is.isEditRange(r)).toBe(true) expect(Is.isEditRange({ insert: r, replace: r })).toBe(true) }) it('should check command', () => { expect(Is.isCommand(undefined)).toBe(false) expect(Is.isCommand({})).toBe(false) expect(Is.isCommand({ title: '', command: '' })).toBe(false) expect(Is.isCommand({ title: 'title', command: 'cmd' })).toBe(true) }) it('should check array', () => { expect(Is.array(false)).toBe(false) }) it('should check empty object', () => { expect(Is.emptyObject(false)).toBe(false) expect(Is.emptyObject({})).toBe(true) expect(Is.emptyObject({ x: 1 })).toBe(false) }) it('should check typed array', () => { let arr = new Array(10) arr.fill(1) expect(Is.typedArray(arr, v => { return v >= 0 })).toBe(true) }) }) describe('lodash', () => { it('should set defaults', () => { let res = lodash.defaults({ a: 1 }, { b: 2 }, { a: 3 }, null) expect(res).toEqual({ a: 1, b: 2 }) res = lodash.defaults({}, { constructor: 'fn' }) expect(res.constructor).toBe('fn') }) }) describe('color', () => { it('should check dark color', () => { expect(color.isDark(Color.create(0.03, 0.01, 0.01, 0))).toBe(true) }) }) describe('parseAnsiHighlights', () => { function testColorHighlight(highlight: string, hlGroup: string, markdown = true) { let text = `${style[highlight].open}text${style[highlight].close}` let res = parseAnsiHighlights(text, markdown) expect(res.highlights.length).toBeGreaterThan(0) let o = res.highlights.find(o => o.hlGroup == hlGroup) expect(o).toBeDefined() } it('should parse foreground color', () => { testColorHighlight('yellow', 'CocMarkdownCode') testColorHighlight('blue', 'CocMarkdownLink') testColorHighlight('magenta', 'CocMarkdownHeader') testColorHighlight('green', 'CocListFgGreen') testColorHighlight('green', 'CocListFgGreen', false) }) it('should parse background color', () => { let text = `${style.bgRed.open}text${style.bgRed.close}` let res = parseAnsiHighlights(text, false) expect(res.highlights.length).toBeGreaterThan(0) expect(res.highlights[0].hlGroup).toBe('CocListBgRed') text = '\u001b[33m\u001b[mnormal' res = parseAnsiHighlights(text, false) expect(res.highlights.length).toBe(0) }) it('should parse foreground and background', () => { let text = `${style.bgRed.open}${style.blue.open}text${style.blue.close}${style.bgRed.close}` let res = parseAnsiHighlights(text, true) expect(res.highlights.length).toBeGreaterThan(0) expect(res.highlights[0].hlGroup).toBe('CocListBlueRed') }) it('should erase char', () => { let text = `foo\u0008bar` let res = parseAnsiHighlights(text, true) expect(res.line).toBe('fobar') text = `${style.bgRed.open}foo${style.bgRed.close}\u0008bar` res = parseAnsiHighlights(text, true) expect(res.line).toBe('fobar') text = `${style.bgRed.open}f${style.bgRed.close}\u0008bar` res = parseAnsiHighlights(text, true) expect(res.line).toBe('bar') }) it('should not throw for bad control character', () => { let text = '\x1bafoo' let res = parseAnsiHighlights(text) expect(res.line).toBeDefined() text = '\x1b[33;44mabc\x1b[33,44m' res = parseAnsiHighlights(text) expect(res.line).toBe('abc') }) }) describe('Arrays', () => { it('distinct()', () => { function compare(a: string): string { return a } assert.deepStrictEqual(arrays.distinct(['32', '4', '5'], compare), ['32', '4', '5']) assert.deepStrictEqual(arrays.distinct(['32', '4', '5', '4'], compare), ['32', '4', '5']) assert.deepStrictEqual(arrays.distinct(['32', 'constructor', '5', '1'], compare), ['32', 'constructor', '5', '1']) assert.deepStrictEqual(arrays.distinct(['32', 'constructor', 'proto', 'proto', 'constructor'], compare), ['32', 'constructor', 'proto']) assert.deepStrictEqual(arrays.distinct(['32', '4', '5', '32', '4', '5', '32', '4', '5', '5'], compare), ['32', '4', '5']) }) it('tail()', () => { assert.strictEqual(arrays.tail([1, 2, 3]), 3) }) it('intersect()', () => { assert.ok(!arrays.intersect([1, 2, 3], [4, 5])) }) it('isFalsyOrEmpty()', () => { assert.ok(arrays.isFalsyOrEmpty([])) assert.ok(arrays.isFalsyOrEmpty(false)) assert.ok(!arrays.isFalsyOrEmpty([1])) }) it('should check intable', () => { assert.ok(arrays.intable(1, [[0, 1], [2, 3], [4, 5]])) assert.ok(arrays.intable(2, [[0, 1], [4, 6], [8, 9]]) === false) assert.ok(arrays.intable(5, [[0, 1], [2, 3], [4, 5]])) assert.ok(arrays.intable(6, [[0, 1], [2, 3], [4, 5]]) === false) }) it('binarySearch()', () => { let comparator = (a, b) => a - b assert.ok(typeof arrays.binarySearch2 === 'function') assert.ok(arrays.binarySearch([1, 2, 3], 2, comparator) == 1) assert.ok(arrays.binarySearch([1, 2, 3, 4], 3, comparator) == 2) assert.ok(arrays.binarySearch([1, 2, 3, 4], 1, comparator) == 0) assert.ok(arrays.binarySearch([1, 2, 3, 4], 0.5, comparator) == -1) assert.ok(arrays.binarySearch([1, 2, 3, 5], 6, comparator) == -5) }) it('toArray()', () => { assert.deepStrictEqual(arrays.toArray(1), [1]) assert.deepStrictEqual(arrays.toArray(null), []) assert.deepStrictEqual(arrays.toArray(undefined), []) assert.deepStrictEqual(arrays.toArray([1, 2]), [1, 2]) }) it('findIndex()', () => { expect(arrays.findIndex([1, 2, 3, 4], 3, 1)).toBe(2) expect(arrays.findIndex([1, 2, 3, 4], 3)).toBe(2) }) it('group()', () => { let res = arrays.group([1, 2, 3, 4, 5], 3) assert.deepStrictEqual(res, [[1, 2, 3], [4, 5]]) }) it('groupBy()', () => { let res = arrays.groupBy([0, 0, 3, 4], v => v != 0) assert.deepStrictEqual(res, [[3, 4], [0, 0]]) }) it('lastIndex()', () => { let res = arrays.lastIndex([1, 2, 3], x => x < 3) assert.strictEqual(res, 1) }) it('flatMap()', () => { let objs: { [key: string]: number[] }[] = [{ x: [1, 2] }, { y: [3, 4] }, { z: [5, 6] }] function values(item: { [key: string]: number[] }): number[] { return Object.keys(item).reduce((p, c) => p.concat(item[c]), []) } let res = arrays.flatMap(objs, values) assert.deepStrictEqual(res, [1, 2, 3, 4, 5, 6]) }) it('addSortedArray()', () => { expect(arrays.addSortedArray('a', ['d', 'e'])).toEqual(['a', 'd', 'e']) expect(arrays.addSortedArray('f', ['d', 'e'])).toEqual(['d', 'e', 'f']) expect(arrays.addSortedArray('d', ['d', 'e'])).toEqual(['d', 'e']) expect(arrays.addSortedArray('e', ['d', 'f'])).toEqual(['d', 'e', 'f']) }) }) describe('Position', () => { function addPosition(position: Position, line: number, character: number): Position { return Position.create(position.line + line, position.character + character) } test('samePosition', () => { let pos = Position.create(0, 0) expect(positions.samePosition(pos, Position.create(0, 0))).toBe(true) }) test('adjacentPosition', () => { let pos = Position.create(0, 0) expect(positions.adjacentPosition(pos, Range.create(0, 0, 0, 1))).toBe(true) expect(positions.adjacentPosition(pos, Range.create(1, 0, 1, 1))).toBe(false) pos = Position.create(1, 1) expect(positions.adjacentPosition(pos, Range.create(1, 0, 1, 1))).toBe(true) }) test('equalsRange', () => { let r = Range.create(0, 0, 0, 1) expect(positions.equalsRange(r, r)).toBe(true) expect(positions.equalsRange(r, Range.create(0, 1, 0, 1))).toBe(false) }) test('compareRangesUsingStarts', () => { let pos = Position.create(3, 3) let range = Range.create(pos, pos) const r = (a, b, c, d) => { return Range.create(a, b, c, d) } expect(positions.compareRangesUsingStarts(range, range)).toBe(0) expect(positions.compareRangesUsingStarts(r(1, 1, 1, 1), range)).toBeLessThan(0) expect(positions.compareRangesUsingStarts(r(3, 3, 3, 4), range)).toBeGreaterThan(0) expect(positions.compareRangesUsingStarts(r(4, 0, 4, 1), range)).toBeGreaterThan(0) expect(positions.compareRangesUsingStarts(r(3, 3, 4, 1), range)).toBeGreaterThan(0) }) test('adjustRangePosition', () => { let pos = Position.create(3, 3) expect(positions.adjustRangePosition(Range.create(0, 0, 1, 0), pos)).toEqual(Range.create(3, 3, 4, 0)) }) test('rangeInRange', () => { let pos = Position.create(0, 0) let r = Range.create(pos, pos) expect(positions.rangeInRange(r, r)).toBe(true) expect(positions.rangeInRange(r, Range.create(addPosition(pos, 1, 0), pos))).toBe(false) expect(positions.rangeInRange(Range.create(0, 1, 0, 1), Range.create(0, 0, 0, 1))).toBe(true) }) test('rangeOverlap', () => { let r = Range.create(0, 0, 0, 0) expect(positions.rangeOverlap(r, Range.create(0, 0, 0, 0))).toBe(false) expect(positions.rangeOverlap(Range.create(0, 0, 0, 10), Range.create(0, 1, 0, 2))).toBe(true) expect(positions.rangeOverlap(Range.create(0, 0, 0, 1), Range.create(0, 1, 0, 2))).toBe(false) expect(positions.rangeOverlap(Range.create(0, 1, 0, 2), Range.create(0, 0, 0, 1))).toBe(false) expect(positions.rangeOverlap(Range.create(0, 0, 0, 1), Range.create(0, 2, 0, 3))).toBe(false) }) test('rangeAdjacent', () => { let r = Range.create(1, 1, 1, 2) expect(positions.rangeAdjacent(r, Range.create(0, 0, 0, 0))).toBe(false) expect(positions.rangeAdjacent(r, Range.create(1, 1, 1, 3))).toBe(false) expect(positions.rangeAdjacent(r, Range.create(0, 0, 1, 1))).toBe(true) expect(positions.rangeAdjacent(r, Range.create(1, 2, 1, 4))).toBe(true) }) test('positionInRange', () => { let pos = Position.create(0, 0) let r = Range.create(pos, pos) expect(positions.positionInRange(pos, r)).toBe(0) pos = Position.create(0, 1) r = Range.create(0, 0, 0, 3) expect(positions.positionInRange(pos, r)).toBe(0) }) test('comparePosition', () => { let pos = Position.create(0, 0) expect(positions.comparePosition(pos, pos)).toBe(0) }) test('should get start end position by content', () => { expect(positions.getEnd(Position.create(0, 0), 'foo')).toEqual({ line: 0, character: 3 }) expect(positions.getEnd(Position.create(0, 1), 'foo\nbar')).toEqual({ line: 1, character: 3 }) }) test('isSingleLine', () => { let pos = Position.create(0, 0) let r = Range.create(pos, pos) expect(positions.isSingleLine(r)).toBe(true) }) test('toValidRange', () => { expect(positions.toValidRange(Range.create(1, 0, 0, 1))).toEqual(Range.create(0, 1, 1, 0)) expect(positions.toValidRange({ start: { line: -1, character: -1 }, end: { line: -1, character: -1 }, })).toEqual(Range.create(0, 0, 0, 0)) }) }) describe('utility', () => { it('should not throw for invalid ms', async () => { await wait(-1) }) it('should disposeAll', () => { disposeAll([undefined, undefined]) }) it('should wait with token', async () => { let res = await waitWithToken(1, CancellationToken.None) expect(res).toBe(false) let tokenSource = new CancellationTokenSource() let token = tokenSource.token let p = waitWithToken(200, token) await wait(1) tokenSource.cancel() res = await p expect(res).toBe(true) res = await waitWithToken(10, CancellationToken.Cancelled) expect(res).toBe(true) res = await waitWithToken(0, CancellationToken.None) expect(res).toBe(true) }) it('should check executable', () => { let res = executable('command_not_exists') expect(res).toBe(false) }) it('should check isRunning', () => { expect(isRunning(process.pid)).toBe(true) let spy = jest.spyOn(process, 'kill').mockImplementation(() => { let e = new Error() as any e.code = 'EPERM' throw e }) expect(isRunning(process.pid)).toBe(true) spy.mockRestore() }) it('should run command on windows', async () => { await runCommand('echo 1') await runCommand('echo 1', { cwd: __dirname }, 1, true) }) it('should run command with timeout', async () => { await expect(async () => { await runCommand('sleep 2', { cwd: __dirname }, 0.01) }).rejects.toThrow(errors.CancellationError) }) it('should run command with Cancellation token', async () => { let tokenSource = new CancellationTokenSource() let token = tokenSource.token setTimeout(() => { tokenSource.cancel() }, 20) await expect(async () => { await runCommand('sleep 2', { cwd: __dirname, encoding: 'unknown' }, token) }).rejects.toThrow(errors.CancellationError) }) it('should run command with encoding support', async () => { let res = await runCommand('echo "\\xc4\\xe3\\x0a"', { cwd: __dirname, encoding: 'cp936' }, 1, true) expect(res.length).toBeGreaterThan(0) }) it('should throw on command error', async () => { await expect(async () => { await runCommand('command_not_exists', { cwd: __dirname }) }).rejects.toThrow(Error) }) it('should resolve concurrent with empty task', async () => { let fn = jest.fn() await concurrent([], fn, 3) expect(fn).toHaveBeenCalledTimes(0) }) it('should run concurrent', async () => { let res: number[] = [] let fn = (n: number): Promise => { return new Promise(resolve => { setTimeout(() => { res.push(n) resolve() }, n * 10) }) } let arr = [5, 4, 3, 6, 8] let ts = Date.now() await concurrent(arr, fn, 3) let dt = Date.now() - ts expect(dt).toBeGreaterThanOrEqual(100) expect(res).toEqual([3, 4, 5, 6, 8]) }) it('should delay function #1', () => { let times = 0 let fn = () => { times++ } let delied = delay(fn, 50) delied() delied(100) expect(times).toBe(0) delied.clear() }) it('should delay function #2', async () => { let times = 0 let fn = () => { times++ } let delied = delay(fn, 50) delied(100) delied(10) await helper.wait(50) expect(times).toBe(1) }) }) describe('fuzzy match test', () => { it('should be fuzzy match', () => { let needle = 'aBc' let codes = fuzzy.getCharCodes(needle) expect(fuzzy.fuzzyMatch(codes, 'abc')).toBeFalsy() expect(fuzzy.fuzzyMatch(codes, 'ab')).toBeFalsy() expect(fuzzy.fuzzyMatch(codes, 'addbdd')).toBeFalsy() expect(fuzzy.fuzzyMatch(codes, 'abbbBc')).toBeTruthy() expect(fuzzy.fuzzyMatch(codes, 'daBc')).toBeTruthy() expect(fuzzy.fuzzyMatch(codes, 'ABCz')).toBeTruthy() expect(fuzzy.fuzzyMatch(codes, 'axy')).toBeFalsy() }) it('should be fuzzy for character', () => { expect(fuzzy.fuzzyChar('a', 'a')).toBeTruthy() expect(fuzzy.fuzzyChar('a', 'A')).toBeTruthy() expect(fuzzy.fuzzyChar('z', 'z')).toBeTruthy() expect(fuzzy.fuzzyChar('z', 'Z')).toBeTruthy() expect(fuzzy.fuzzyChar('A', 'a')).toBeFalsy() expect(fuzzy.fuzzyChar('A', 'A')).toBeTruthy() expect(fuzzy.fuzzyChar('Z', 'z')).toBeFalsy() expect(fuzzy.fuzzyChar('Z', 'Z')).toBeTruthy() expect(fuzzy.fuzzyChar('Z', 'z', true)).toBeTruthy() expect(fuzzy.fuzzyChar('i', 'İ')).toBeTruthy() expect(fuzzy.fuzzyChar('a', 'İ')).toBeFalsy() expect(fuzzy.fuzzyChar('i', 'İ', true)).toBeTruthy() expect(fuzzy.fuzzyChar('İ', 'i')).toBeFalsy() expect(fuzzy.fuzzyChar('İ', 'i', true)).toBeTruthy() expect(fuzzy.fuzzyChar('Ᾰ', 'ᾰ', true)).toBeTruthy() expect(fuzzy.fuzzyChar('ᾰ', 'Ᾰ')).toBeTruthy() }) }) describe('object test', () => { it('mixin should recursive', () => { let res = objects.mixin({ a: { b: 1 } }, { a: { c: 2 }, d: 3 }) expect(res.a.b).toBe(1) expect(res.a.c).toBe(2) expect(res.d).toBe(3) res = objects.mixin({}, true) expect(res).toEqual({}) res = objects.mixin({ x: 1 }, { x: 2 }, false) expect(res).toEqual({ x: 1 }) res = objects.mixin(Date, {}) expect(res).toEqual({}) res = objects.mixin({ x: 3, y: new Date() }, { y: 4 }, true) expect(res).toEqual({ x: 3, y: 4 }) }) it('should deep clone', () => { let re = new RegExp('a', 'g') expect(objects.deepClone(re)).toBe(re) }) it('should change to readonly', () => { let obj = { x: 1 } let res = objects.toReadonly(obj) let fn = () => { res.x = 3 } expect(fn).toThrow() }) it('should not deep freeze', () => { objects.deepFreeze(false) objects.deepFreeze(true) }) it('should check equals', () => { expect(objects.equals(false, 1)).toBe(false) expect(objects.equals([1], {})).toBe(false) expect(objects.equals([1, 2], [1, 3])).toBe(false) }) it('should check empty object', () => { expect(objects.isEmpty({})).toBe(true) expect(objects.isEmpty([])).toBe(true) expect(objects.isEmpty(null)).toBe(true) expect(objects.isEmpty({ x: 1 })).toBe(false) }) it('should omit null and undefined properties', () => { expect(objects.omitNullUndefined({ a: 1, b: null, c: undefined, d: "text" })).toEqual({ a: 1, d: 'text' }) }) it('should deepIterate', () => { let obj = { x: 1, $ref: '#1', items: [{ obj: [{ y: 2, $ref: '#2' }, 4] }, { $ref: '#3' }] } let vals: string[] = [] objects.deepIterate(obj, (obj, key) => { if (key === '$ref') { vals.push(obj[key]) } }) expect(vals).toEqual(['#1', '#2', '#3']) }) }) describe('ansiparse', () => { it('ansiparse #1', () => { let str = '\u001b[33mText\u001b[mnormal' let res = ansiparse(str) expect(res).toEqual([{ foreground: 'yellow', text: 'Text' }, { text: 'normal' }]) }) it('ansiparse #2', () => { let str = '\u001b[33m\u001b[mText' let res = ansiparse(str) expect(res).toEqual([ { foreground: 'yellow', text: '' }, { text: 'Text' }]) }) it('ansiparse #3', () => { let str = 'this.\u001b[0m\u001b[31m\u001b[1mhistory\u001b[0m.add()' let res = ansiparse(str) expect(res[1]).toEqual({ foreground: 'red', bold: true, text: 'history' }) }) }) describe('Mutex', () => { it('mutex run in serial', async () => { let lastTs: number let fn = () => new Promise(resolve => { if (lastTs) { let dt = Date.now() - lastTs expect(dt).toBeGreaterThanOrEqual(2) } lastTs = Date.now() setTimeout(() => { resolve() }, 3) }) let mutex = new Mutex() await Promise.all([ mutex.use(fn), mutex.use(fn), mutex.use(fn) ]) }) it('mutex run after job finish', async () => { let count = 0 let fn = () => new Promise(resolve => { count = count + 1 setTimeout(() => { resolve() }, 10) }) let mutex = new Mutex() await mutex.use(fn) await helper.wait(1) await mutex.use(fn) expect(count).toBe(2) }) it('should release on reject', async () => { let mutex = new Mutex() let err try { await mutex.use(() => { return Promise.reject(new Error('err')) }) } catch (e) { err = e } expect(err).toBeDefined() expect(mutex.busy).toBe(false) }) }) describe('Sequence', () => { it('should run sequence', async () => { let s = new Sequence() let res: number[] = [] s.run(async () => { await helper.wait(3) res.push(0) }) s.run(async () => { await helper.wait(2) res.push(1) }) s.run(async () => { await helper.wait(1) res.push(2) }) await s.waitFinish() expect(res).toEqual([0, 1, 2]) }) it('should cancel sequence', async () => { let s = new Sequence() let res: number[] = [] s.run(async () => { await helper.wait(10) res.push(0) }) s.run(async () => { await helper.wait(20) res.push(1) }) s.cancel() await s.waitFinish() expect(res).toEqual([]) }) }) describe('terminate', () => { it('should terminate process', async () => { let cwd = process.cwd() let child = spawn('sleep', ['3'], { cwd, detached: true }) let res = terminate(child, cwd) expect(res).toBe(true) await helper.waitValue(() => { return child.connected }, false) terminate(child, cwd) terminate({ killed: true } as any, cwd) }) it('should terminate on other platform', () => { let child = spawn('ls', [], { detached: true }) let res = terminate(child, process.cwd(), platform.Platform.Windows) expect(res).toBe(false) res = terminate(child, undefined, platform.Platform.Windows) expect(res).toBe(false) res = terminate(child, process.cwd(), platform.Platform.Unknown) expect(res).toBe(true) let spy: any = jest.spyOn(cp, 'execFileSync').mockImplementation(() => { return undefined }) child = spawn('ls', [], { detached: true }) res = terminate(child, process.cwd(), platform.Platform.Windows) expect(res).toBe(true) spy.mockRestore() spy = jest.spyOn(cp, 'spawnSync').mockImplementation(() => { throw new Error('bad') }) child = spawn('ls', [], { detached: true }) res = terminate(child, process.cwd(), platform.Platform.Linux) expect(res).toBe(false) spy.mockRestore() spy = jest.spyOn(cp, 'spawnSync').mockImplementation(() => { return { error: new Error('bad') } as any }) child = spawn('ls', [], { detached: true }) res = terminate(child, process.cwd(), platform.Platform.Linux) expect(res).toBe(false) spy.mockRestore() }) }) describe('diff', () => { describe('diff lines', () => { function diffLines(oldStr: string, newStr: string): diff.ChangedLines { let oldLines = oldStr.split('\n') return diff.diffLines(oldLines, newStr.split('\n'), oldLines.length - 2) } it('should consider new line insert on insert mode', async () => { let res = diff.getTextEdit(['abc', ''], ['abc', '', ''], Position.create(1, 0), true) expect(res).toEqual(toEdit(0, 3, 0, 3, '\n')) }) it('should get textedit without cursor', () => { let res = diff.getTextEdit(['a', 'b'], ['a', 'b']) expect(res).toBeUndefined() res = diff.getTextEdit(['a', 'b'], ['a', 'b'], Position.create(0, 0)) expect(res).toBeUndefined() res = diff.getTextEdit(['a', 'b'], ['a', 'b', 'c']) expect(res).toEqual(toEdit(2, 0, 2, 0, 'c\n')) res = diff.getTextEdit(['a', 'b', 'c'], ['a']) expect(res).toEqual(toEdit(1, 0, 3, 0, '')) res = diff.getTextEdit(['a', 'b'], ['a', 'd']) expect(res).toEqual(toEdit(1, 0, 1, 1, 'd')) res = diff.getTextEdit(['a', 'b'], ['a', 'd', 'e']) expect(res).toEqual(toEdit(1, 0, 1, 1, 'd\ne')) res = diff.getTextEdit(['a', 'b', 'e'], ['a', 'd', 'e']) expect(res).toEqual(toEdit(1, 0, 1, 1, 'd')) res = diff.getTextEdit(['a', 'b', 'e'], ['e']) expect(res).toEqual(toEdit(0, 0, 2, 0, '')) res = diff.getTextEdit(['a', 'b', 'e'], ['d', 'c', 'a', 'b', 'e']) expect(res).toEqual(toEdit(0, 0, 0, 0, 'd\nc\n')) res = diff.getTextEdit(['a', 'b'], ['a', 'b', '']) expect(res).toEqual(toEdit(2, 0, 2, 0, '\n')) res = diff.getTextEdit(['a', 'b'], ['a', 'b', '', '']) expect(res).toEqual(toEdit(2, 0, 2, 0, '\n\n')) }) it('should reduceTextEdit', () => { let res = diff.reduceReplaceEdit(TextEdit.replace(Range.create(0, 0, 3, 1), 'abd'), 'a\nb\nc\nd', Position.create(0, 1)) expect(res).toEqual(TextEdit.replace(Range.create(0, 1, 3, 0), 'b')) res = diff.reduceReplaceEdit(TextEdit.replace(Range.create(3, 1, 3, 9), ' '.repeat(5)), ' '.repeat(8), Position.create(3, 3)) expect(res).toEqual(TextEdit.replace(Range.create(3, 3, 3, 6), '')) res = diff.reduceReplaceEdit(TextEdit.replace(Range.create(3, 1, 3, 4), ' '.repeat(5)), ' '.repeat(3), Position.create(3, 3)) expect(res).toEqual(TextEdit.replace(Range.create(3, 1, 3, 1), ' ')) res = diff.reduceReplaceEdit(TextEdit.replace(Range.create(3, 1, 3, 4), 'x'.repeat(5)), ' '.repeat(3), Position.create(3, 3)) expect(res).toEqual(TextEdit.replace(Range.create(3, 1, 3, 4), 'x'.repeat(5))) res = diff.reduceReplaceEdit(TextEdit.replace(Range.create(1, 0, 2, 0), 'd\n'), 'b\n') expect(res).toEqual(TextEdit.replace(Range.create(1, 0, 1, 1), 'd')) }) it('should get textedit for single line change', () => { let res = diff.getTextEdit(['foo', 'c'], ['', 'c'], Position.create(0, 0), false) expect(res).toEqual(toEdit(0, 0, 0, 3, '')) res = diff.getTextEdit([''], ['foo'], Position.create(0, 0), false) expect(res).toEqual(toEdit(0, 0, 0, 0, 'foo')) res = diff.getTextEdit(['foo bar'], ['foo r'], Position.create(0, 4), false) expect(res).toEqual(toEdit(0, 4, 0, 6, '')) res = diff.getTextEdit(['f'], ['foo f'], Position.create(0, 0), false) expect(res).toEqual(toEdit(0, 0, 0, 0, 'foo ')) res = diff.getTextEdit([' foo '], [' bar '], Position.create(0, 0), false) expect(res).toEqual(toEdit(0, 1, 0, 4, 'bar')) res = diff.getTextEdit(['foo'], ['bar'], Position.create(0, 0), true) expect(res).toEqual(toEdit(0, 0, 0, 3, 'bar')) res = diff.getTextEdit(['aa'], ['aaaa'], Position.create(0, 1), true) expect(res).toEqual(toEdit(0, 0, 0, 0, 'aa')) }) it('should diff changed lines', () => { let res = diffLines('a\n', 'b\n') expect(res).toEqual({ start: 0, end: 1, replacement: ['b'] }) res = diff.diffLines(['a', 'b'], ['c', 'd', 'a', 'b'], -1) expect(res).toEqual({ start: 0, end: 0, replacement: ['c', 'd'] }) }) it('should diff added lines', () => { let res = diffLines('a\n', 'a\nb\n') expect(res).toEqual({ start: 1, end: 1, replacement: ['b'] }) }) it('should diff remove lines', () => { let res = diffLines('a\n\n', 'a\n') expect(res).toEqual({ start: 1, end: 2, replacement: [] }) }) it('should diff remove multiple lines', () => { let res = diffLines('a\n\n\n', 'a\n') expect(res).toEqual({ start: 1, end: 3, replacement: [] }) }) it('should diff removed line', () => { let res = diffLines('a\n\n\nb', 'a\n\nb') expect(res).toEqual({ start: 2, end: 3, replacement: [] }) }) it('should reduce changed lines', () => { let res = diff.diffLines(['a', 'b', 'c'], ['a', 'b', 'c', 'd'], 0) expect(res).toEqual({ start: 3, end: 3, replacement: ['d'] }) }) }) describe('get common prefix & suffix', () => { it('should getCommonPrefixLen', () => { expect(diff.getCommonPrefixLen('aa', 'abc', 0)).toBe(0) expect(diff.getCommonPrefixLen(' '.repeat(5), ' '.repeat(10), 4)).toBe(4) expect(diff.getCommonPrefixLen('xy', 'dy', 2)).toBe(0) }) it('should getCommonSuffixLen', () => { expect(diff.getCommonSuffixLen('aa', 'aa', 0)).toBe(0) expect(diff.getCommonSuffixLen('aa', 'ab', 2)).toBe(0) expect(diff.getCommonSuffixLen(' '.repeat(3), ' '.repeat(5), 2)).toBe(2) }) }) describe('patch line', () => { it('should patch line', () => { let res = diff.patchLine('foo', 'bar foo bar') expect(res.length).toBe(7) expect(res).toBe(' foo') res = diff.patchLine('foo', 'foo') expect(res).toBe('foo') res = diff.patchLine('foo', 'oo') expect(res).toBe('oo') }) }) function blockMilliseconds(ms: number): void { let ts = Date.now() let i = 0 while (true) { if (Date.now() - ts > ms) { break } i++ } } describe('async', () => { it('should do filter', async () => { await filter([], () => true, () => {}) await filter([{ label: 'a' }, { label: 'b' }, { label: 'c' }], v => { return { code: v.label.charCodeAt(0) } }, (items, done) => { expect(items.length).toBe(3) expect(done).toBe(true) }) let n = 0 let res: string[] = [] let finished: boolean await filter(['a', 'b', 'c'], () => { blockMilliseconds(30) return true }, (items, done) => { n++ res.push(...items) finished = done }) expect(n).toBe(3) expect(res).toEqual(['a', 'b', 'c']) expect(finished).toEqual(true) }) it('should cancel filter when possible', async () => { let tokenSource = new CancellationTokenSource() let token = tokenSource.token process.nextTick(() => { tokenSource.cancel() }) await filter([1, 2, 3, 4, 5, 6, 7, 8], i => { if (i > 1) { let ts = Date.now() while (true) { if (Date.now() - ts > 40) break } } return true }, (_, done) => { expect(done).toBeFalsy() }, token) }) it('should perform async forEach', async () => { await forEach([], () => {}) let res = [] await forEach([1, 2], x => res.push(x)) expect(res).toEqual([1, 2]) let result = [] const items = Array(1024 * 20).fill(1) // 足够大的数组以确保会yield await forEach(items, () => result.push(helper.generateRandomHash()), undefined, { yieldAfter: 5 }) expect(result.length).toBe(items.length) result = [] await forEach(items, () => result.push(helper.generateRandomHash()), undefined) expect(result.length).toBe(items.length) // it should cancel with callback called. let tokenSource = new CancellationTokenSource() let token = tokenSource.token let called = false let cb = () => { tokenSource.cancel() called = true } result = [] await forEach(items, () => result.push(helper.generateRandomHash()), token, { yieldAfter: 1, yieldCallback: cb }) expect(called).toBe(true) expect(result.length).toBeLessThan(items.length) }) it('should map with empty array should return empty array', async () => { const result = await map([], x => x * 2) expect(result).toEqual([]) }) it('should map correct transform items', async () => { const items = Array(1024 * 20).fill(1) // 足够大的数组以确保会yield const result = await map(items, () => helper.generateRandomHash()) expect(result.length).toBe(items.length) }) it('should map yieldCallback when yielding', async () => { const items = Array(1024 * 20).fill(1) // 足够大的数组以确保会yield let tokenSource = new CancellationTokenSource() let token = tokenSource.token let called = false let cb = () => { tokenSource.cancel() called = true } const options: YieldOptions = { yieldCallback: cb, yieldAfter: 5 } await map(items, x => helper.generateRandomHash(), token, options) expect(called).toBe(true) }) it('should cancel on map', async () => { let total = 1024 * 1024 const items = Array(total).fill(1) // 足够大的数组以确保会yield let tokenSource = new CancellationTokenSource() let token = tokenSource.token process.nextTick(() => { tokenSource.cancel() }, 0) let res = await map(items, x => helper.generateRandomHash(), token) expect(res[res.length - 1]).toBeUndefined() }) }) describe('timing', () => { it('should trace', async () => { let t = createTiming('name', 1) t.start() t.start('label') await helper.wait(10) t.stop() t.start() t.stop() }) it('should no timeout', () => { let t = createTiming('name') t.start() t.stop() }) }) }) ================================================ FILE: src/__tests__/modules/window.test.ts ================================================ import { Buffer, Neovim } from '@chemzqm/neovim' import { HighlightItem } from '@chemzqm/neovim/lib/api/Buffer' import { CancellationToken, Disposable, Emitter } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { convertHighlightItem } from '../../core/highlights' import events from '../../events' import Notification, { toButtons, toTitles } from '../../model/notification' import { formatMessage } from '../../model/progress' import { TreeItem, TreeItemCollapsibleState } from '../../tree' import { disposeAll } from '../../util' import window, { Window } from '../../window' import workspace from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let disposables: Disposable[] = [] interface FileNode { filepath: string isFolder?: boolean } beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) disposables = [] }) describe('window', () => { describe('functions', () => { it('should formatMessage', () => { expect(Window).toBeDefined() expect(formatMessage('a', 'b', 1)).toBe('a b 1%') expect(formatMessage(undefined, undefined, 1)).toBe('1%') expect(formatMessage('a', undefined, 0)).toBe('a') }) it('should convert highlight item', () => { let res = convertHighlightItem({ colStart: 0, colEnd: 1, hlGroup: 'Search', lnum: 0, combine: true }) expect(res).toEqual(['Search', 0, 0, 1, 1, 0, 0]) }) it('should get offset', async () => { let buf = await nvim.buffer await nvim.call('setline', [buf.id, ['bar', 'foo']]) await nvim.call('cursor', [2, 2]) let n = await window.getOffset() expect(n).toBe(5) }) it('should get cursor screen position', async () => { let pos = await window.getCursorScreenPosition() expect(pos).toEqual({ row: 0, col: 0 }) }) it('should export terminals', async () => { expect(Array.isArray(window.terminals)).toBe(true) expect(window.onDidOpenTerminal).toBeDefined() expect(window.onDidCloseTerminal).toBeDefined() }) it('should selected range', async () => { await nvim.setLine('foobar') await nvim.command('normal! viw') await nvim.eval(`feedkeys("\\", 'in')`) let range = await window.getSelectedRange('v') expect(range).toEqual({ start: { line: 0, character: 0 }, end: { line: 0, character: 6 } }) }) it('should run terminal command', async () => { let res = await window.runTerminalCommand('ls', __dirname) expect(res.success).toBe(true) res = await window.runTerminalCommand('echo 1', process.cwd(), true) expect(res.success).toBe(true) }) it('should open temimal buffer', async () => { let bufnr = await window.openTerminal('ls', { autoclose: false, keepfocus: false }) let curr = await nvim.eval('bufnr("%")') expect(curr).toBe(bufnr) let buftype = await nvim.eval('&buftype') expect(buftype).toBe('terminal') }) it('should create float factory', async () => { helper.updateConfiguration('coc.preferences.excludeImageLinksInMarkdownDocument', false) helper.updateConfiguration('floatFactory.floatConfig', { winblend: 10, rounded: true, border: true, close: true }) let f = window.createFloatFactory({ modes: ['n', 'i'] }) await f.show([{ content: 'content', filetype: 'txt' }]) let win = await helper.getFloat() expect(win).toBeDefined() let id = await nvim.call('coc#float#get_related', [win.id, 'border', 0]) as number expect(id).toBeGreaterThan(0) }) it('should createStatusBarItem', async () => { let item = window.createStatusBarItem(1, { progress: true }) item.text = 'test' item.show() expect(item.text).toBe('test') expect(item.isProgress).toBe(true) let other = window.createStatusBarItem() other.text = 'bar' other.show() await helper.waitValue(async () => { let res = await nvim.getVar('coc_status') as string return res.includes('bar') }, true) item.hide() item.dispose() other.dispose() }) it('should create outputChannel', () => { let channel = window.createOutputChannel('channel') expect(channel.name).toBe('channel') }) it('should create TreeView instance', async () => { let emitter = new Emitter() let removed = false let treeView = window.createTreeView('files', { treeDataProvider: { onDidChangeTreeData: emitter.event, getChildren: root => { if (root) return undefined if (removed) return [{ filepath: '/foo/a', isFolder: true }] return [{ filepath: '/foo/a', isFolder: true }, { filepath: '/foo/b.js' }] }, getTreeItem: (node: FileNode) => { let { filepath, isFolder } = node return new TreeItem(URI.file(filepath), isFolder ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None) }, } }) disposables.push(emitter) disposables.push(treeView) await treeView.show() let filetype = await nvim.eval('&filetype') expect(filetype).toBe('coctree') }) it('should show outputChannel', async () => { window.createOutputChannel('channel') window.showOutputChannel('channel') let buf = await nvim.buffer let name = await buf.name expect(name).toMatch('channel') }) it('should not show channel not exists', async () => { let buf = await nvim.buffer let bufnr = buf.id window.showOutputChannel('NONE', 'edit') await helper.wait(10) buf = await nvim.buffer expect(buf.id).toBe(bufnr) }) it('should get cursor position', async () => { await nvim.setLine(' ') await nvim.call('cursor', [1, 3]) let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 2 }) }) it('should moveTo position in insert mode', async () => { await nvim.setLine('foo') await nvim.input('i') await window.moveTo({ line: 0, character: 3 }) let col = await nvim.call('col', '.') expect(col).toBe(4) let virtualedit = await nvim.getOption('virtualedit') expect(virtualedit).toBe('') }) it('should choose quickpick', async () => { let p = window.showQuickpick(['a', 'b']) await helper.waitPrompt() await nvim.input('1') await nvim.input('') let res = await p expect(res).toBe(0) }) it('should cancel quickpick', async () => { let p = window.showQuickpick(['a', 'b']) await helper.waitPrompt() await nvim.input('') let res = await p expect(res).toBe(-1) }) it('should show prompt', async () => { let p = window.showPrompt('prompt') await helper.wait(50) await nvim.input('y') let res = await p expect(res).toBe(true) }) it('should show dialog', async () => { let dialog = await window.showDialog({ content: 'foo' }) let winid = await dialog.winid expect(winid).toBeDefined() expect(winid).toBeGreaterThan(1000) }) it('should show menu', async () => { let p = window.showMenuPicker(['a', 'b', 'c'], 'choose item') await helper.waitValue(async () => { return await nvim.call('coc#float#has_float', []) }, 1) await nvim.input('2') let res = await p expect(res).toBe(1) res = await window.showMenuPicker(['foo'], { title: 'title', position: 'center' }, CancellationToken.Cancelled) expect(res).toBe(-1) }) it('should return select items for picker', async () => { let curr = await nvim.call('win_getid') let p = window.showPickerDialog(['foo', 'bar'], 'select') await helper.waitFloat() await helper.waitPrompt() await nvim.input(' ') await nvim.input('') let res = await p let winid = await nvim.call('win_getid') expect(winid).toBe(curr) expect(res).toEqual(['foo']) }) it('should return undefined for picker', async () => { let p = window.showPickerDialog(['foo', 'bar'], 'select') await helper.waitFloat() await helper.waitPrompt() await nvim.input('') let res = await p expect(res).toBeUndefined() }) it('should return undefined when cancelled', async () => { let token = CancellationToken.Cancelled let res = await window.showPickerDialog(['foo', 'bar'], 'select', token) expect(res).toBeUndefined() }) it('should get visible ranges of bufnr', async () => { let buf = await helper.edit('not_exists') let range = await window.getVisibleRanges(buf.id) expect(range.length).toBe(1) let winid = await nvim.call('win_getid') as number range = await window.getVisibleRanges(buf.id, winid) expect(range.length).toBe(1) range = await window.getVisibleRanges(buf.id, 9999) expect(range.length).toBe(0) await nvim.command('enew') range = await window.getVisibleRanges(buf.id) expect(range.length).toBe(0) }) it('should requestInputList', async () => { Object.assign(workspace.env, { lines: 3 }) { let p = window.requestInputList('prompt', ['foo', 'bar', 'abc', 'def']) await helper.waitValue(async () => { let m = await nvim.mode return m.mode }, 'c') await nvim.input('1') let res = await p expect(res).toBe(0) } { let p = window.requestInputList('prompt', ['foo', 'bar', 'abc', 'def']) await helper.waitValue(async () => { let m = await nvim.mode return m.mode }, 'c') await nvim.input('8') let res = await p expect(res).toBe(-1) } }) }) describe('window showMessage', () => { async function ensureNotification(idx: number): Promise { let winid = await helper.waitFloat() await nvim.call('coc#notify#choose', [winid, idx]) } it('should echo lines', async () => { await window.echoLines(['a', 'b']) let ch = await nvim.call('screenchar', [79, 1]) as number let s = String.fromCharCode(ch) expect(s).toBe('a') }) it('should echo multiple lines with truncate', async () => { await window.echoLines(['a', 'b'.repeat(99), 'd', 'e'], true) let ch = await nvim.call('screenchar', [79, 1]) as number let s = String.fromCharCode(ch) expect(s).toBe('a') await window.echoLines(['a', 'b'.repeat(200)], true) }) it('should show messages', async () => { window.showMessage('more') window.showMessage('error', 'error') window.showMessage('warning', 'warning') window.showMessage('moremsg', 'more') }) it('should show message item', async () => { helper.updateConfiguration('coc.preferences.enableMessageDialog', true) let p = window.showInformationMessage('information message', { title: 'first' }, { title: 'second' }) await ensureNotification(0) let res = await p expect(res).toEqual({ title: 'first' }) res = await window.showInformationMessage('information message') expect(res).toBeUndefined() }) it('should show warning message', async () => { helper.updateConfiguration('coc.preferences.enableMessageDialog', true) let p = window.showWarningMessage('warning message', 'first', 'second') await ensureNotification(1) let res = await p expect(res).toBe('second') }) it('should show error message', async () => { helper.updateConfiguration('coc.preferences.enableMessageDialog', true) let p = window.showErrorMessage('error message', 'first', 'second') await ensureNotification(0) let res = await p expect(res).toBe('first') }) it('should show confirm for message', async () => { helper.updateConfiguration('coc.preferences.enableMessageDialog', false) let spy = jest.spyOn(nvim, 'call').mockImplementationOnce((method, _args) => { expect(method).toBe('confirm') return Promise.resolve('2') as any }) let p = window.showInformationMessage('error message', 'first', 'second') let res = await p spy.mockRestore() expect(res).toBe('second') }) it('should use messageDialogKind for confirm mode', async () => { helper.updateConfiguration('coc.preferences.messageDialogKind', 'confirm') let spy = jest.spyOn(nvim, 'call').mockImplementationOnce((method, args) => { expect(method).toBe('confirm') expect(args[0]).toBe('test message') expect(args[1]).toBe('1first\n2second') return Promise.resolve('1') as any }) let p = window.showInformationMessage('test message', 'first', 'second') let res = await p spy.mockRestore() expect(res).toBe('first') }) it('should use messageDialogKind for menu mode', async () => { helper.updateConfiguration('coc.preferences.messageDialogKind', 'menu') let spy = jest.spyOn(window.dialogs, 'showMenuPicker').mockImplementation(() => { return Promise.resolve(1) as any }) let res = await window.notifications._showMessage('Warning', 'test message', ['first', 'second']) expect(spy).toHaveBeenCalledWith(['first', 'second'], { position: 'center', content: 'test message', title: 'Choose an action', borderhighlight: 'CocWarningFloat' }) expect(res).toBe('second') spy.mockRestore() }) it('should use messageDialogKind for notification mode', async () => { helper.updateConfiguration('coc.preferences.messageDialogKind', 'notification') let p = window.showInformationMessage('notification message', 'first', 'second') await ensureNotification(0) let res = await p expect(res).toBe('first') }) it('should echo error messages regardless of messageDialogKind', async () => { helper.updateConfiguration('coc.preferences.messageDialogKind', 'menu') let spy = jest.spyOn(window.notifications, 'echoMessages') await window.showErrorMessage('error message') expect(spy).toHaveBeenCalledWith('error message', 'error') spy.mockRestore() }) it('should echo messages without items regardless of messageDialogKind', async () => { helper.updateConfiguration('coc.preferences.messageDialogKind', 'confirm') let spy = jest.spyOn(window.notifications, 'echoMessages') await window.showInformationMessage('info message') expect(spy).toHaveBeenCalledWith('info message', 'more') spy.mockRestore() }) it('should echo messages without items when configured messageReportKind', async () => { helper.updateConfiguration('coc.preferences.messageReportKind', 'echo') let spy = jest.spyOn(window.notifications, 'echoMessages') await window.showInformationMessage('info message') expect(spy).toHaveBeenCalledWith('info message', 'more') spy.mockRestore() }) it('should use notification messages without items when configured messageReportKind', async () => { helper.updateConfiguration('coc.preferences.messageReportKind', 'notification') let spy = jest.spyOn(window.notifications, 'createNotification') await window.showInformationMessage('info message') expect(spy).toHaveBeenCalledWith('info', 'info message', []) spy.mockRestore() }) it('should handle unexpected messageReportKind', async () => { helper.updateConfiguration('coc.preferences.messageReportKind', 'invalid') let p = window.showInformationMessage('invalid info message') await expect(p).rejects.toThrow('Unexpected messageReportKind: invalid') }) it('should handle unexpected messageDialogKind', async () => { helper.updateConfiguration('coc.preferences.messageDialogKind', 'invalid') let p = window.showInformationMessage('test message', 'first', 'second') await expect(p).rejects.toThrow('Unexpected messageDialogKind: invalid') }) it('should respect enableMessageDialog for backward compatibility', async () => { helper.updateConfiguration('coc.preferences.enableMessageDialog', true) helper.updateConfiguration('coc.preferences.messageDialogKind', 'confirm') let p = window.showInformationMessage('notification message', 'first', 'second') await ensureNotification(0) let res = await p expect(res).toBe('first') }) }) describe('window notifications', () => { it('should toButtons', () => { expect(toButtons(['foo', 'bar']).length).toBe(2) }) it('should toTitles', () => { expect(toTitles(['foo', 'bar']).length).toBe(2) expect(toTitles([{ title: 'foo' }]).length).toBe(1) }) it('should show notification with options', async () => { await window.showNotification({ content: 'my notification', title: 'title', }) let ids = await nvim.call('coc#float#get_float_win_list') as number[] expect(ids.length).toBe(1) let win = nvim.createWindow(ids[0]) let kind = await win.getVar('kind') expect(kind).toBe('notification') let winid = await nvim.call('coc#float#get_related', [win.id, 'border']) let bufnr = await nvim.call('winbufnr', [winid]) as number let buf = nvim.createBuffer(bufnr) let lines = await buf.lines expect(lines[0].includes('title')).toBe(true) }) it('should ignore events of other buffers', async () => { let bufnr = workspace.bufnr let notification = new Notification(nvim, {}) await events.fire('BufWinLeave', [bufnr + 1]) await events.fire('FloatBtnClick', [bufnr + 1, 1]) notification.dispose() }) it('should show notification without border', async () => { helper.updateConfiguration('notification.border', false) await window.showNotification({ content: 'my notification', title: 'title', }) let win = await helper.getFloat() let height = await nvim.call('coc#float#get_height', [win.id]) expect(height).toBe(2) }) it('should show status line progress by default', async () => { let called = 0 let text: string setTimeout(async () => { text = await nvim.getVar('coc_status') as string }, 10) let res = await window.withProgress({ title: 'Processing' }, progress => { let n = 0 return new Promise(resolve => { let interval = setInterval(() => { progress.report({ message: 'progress', increment: 1 }) n = n + 10 called = called + 1 if (n == 30) { clearInterval(interval) resolve('done') } }, 10) }) }) expect(text).toMatch('Processing') expect(called).toBeGreaterThan(1) expect(res).toBe('done') }) it('should show progress notification', async () => { helper.updateConfiguration('notification.statusLineProgress', false) let called = 0 let res = await window.withProgress({ title: 'Downloading', cancellable: true }, (progress, token) => { let n = 0 return new Promise(resolve => { let interval = setInterval(() => { progress.report({ message: 'progress', increment: 1 }) n = n + 10 called = called + 1 if (n == 100) { clearInterval(interval) resolve('done') } }, 10) token.onCancellationRequested(() => { clearInterval(interval) resolve(undefined) }) }) }) expect(called).toBeGreaterThan(8) expect(res).toBe('done') }) it('should cancel progress notification on window close', async () => { helper.updateConfiguration('notification.statusLineProgress', false) let called = 0 let p = window.withProgress({ title: 'Downloading', cancellable: true }, (progress, token) => { let n = 0 return new Promise(resolve => { let interval = setInterval(() => { progress.report({ message: 'progress', increment: 1 }) n = n + 10 called = called + 1 if (n == 100) { clearInterval(interval) resolve('done') } }, 10) token.onCancellationRequested(() => { clearInterval(interval) resolve(undefined) }) }) }) await helper.wait(30) await nvim.call('coc#float#close_all', []) let res = await p expect(called).toBeLessThan(10) expect(res).toBe(undefined) }) it('should cancel progress when resolved', async () => { helper.updateConfiguration('notification.statusLineProgress', false) let called = 0 let p = window.withProgress({ title: 'Process' }, () => { called = called + 1 return Promise.resolve() }) await p let win = await helper.getFloat() if (win) { let res = await nvim.call('coc#window#get_var', [win.id, 'closing']) expect(res).toBe(1) } expect(called).toBe(1) }) it('should be disabled by configuration', async () => { helper.updateConfiguration('notification.statusLineProgress', false) helper.updateConfiguration('notification.disabledProgressSources', ['test']) let p = window.withProgress({ title: 'Downloading', source: 'test' }, (progress, token) => { let n = 0 return new Promise(resolve => { let interval = setInterval(() => { progress.report({ message: 'progress', increment: 1 }) n = n + 1 if (n == 10) { clearInterval(interval) resolve('done') } }, 10) }) }) await helper.wait(30) let win = await helper.getFloat() expect(win).toBeUndefined() let res = await p expect(res).toBe('done') }) it('should show error message when rejected', async () => { helper.updateConfiguration('notification.statusLineProgress', false) let p = window.withProgress({ title: 'Process' }, () => { return Promise.reject(new Error('Unable to fetch')) }) let res = await p expect(res).toBe(undefined) let cmdline = await helper.getCmdline() expect(cmdline).toMatch(/Unable to fetch/) }) }) describe('diffHighlights', () => { let ns = 'window-test' let priority = 99 let ns_id: number beforeAll(async () => { ns_id = await nvim.call('coc#highlight#create_namespace', [ns]) as number }) async function createFile(content = 'foo\nbar'): Promise { let file = await createTmpFile(content) return await helper.edit(file) } async function setHighlights(hls: HighlightItem[]): Promise { let bufnr = await nvim.call('bufnr', ['%']) as number let arr = hls.map(o => [o.hlGroup, o.lnum, o.colStart, o.colEnd, o.combine === false ? 0 : 1, o.end_incl ? 1 : 0, o.start_incl ? 1 : 0]) await nvim.call('coc#highlight#set', [bufnr, ns, arr, priority]) } it('should return null when canceled', async () => { let buf = await createFile() let items: HighlightItem[] = [] let token = CancellationToken.Cancelled let res = await window.diffHighlights(buf.id, ns, items, undefined, token) expect(res).toBe(null) }) it('should add new highlights', async () => { let buf = await createFile() let items: HighlightItem[] = [{ hlGroup: 'Search', lnum: 0, colStart: 0, colEnd: 3 }] let res = await window.diffHighlights(buf.id, ns, items) expect(res).toBeDefined() expect(res.add.length).toBe(1) await window.applyDiffHighlights(buf.id, ns, priority, res) let markers = await buf.getExtMarks(ns_id, 0, -1, { details: true }) expect(markers.length).toBe(1) expect(markers[0][3].end_col).toBe(3) }) it('should update with new highlights', async () => { let buf = await createFile('foo\nbar\nbaz') let items: HighlightItem[] = [{ hlGroup: 'Search', lnum: 0, colStart: 0, colEnd: 3 }, { hlGroup: 'Search', lnum: 2, colStart: 0, colEnd: 3 }] await setHighlights(items) let newItems: HighlightItem[] = [{ hlGroup: 'Search', lnum: 0, colStart: 0, colEnd: 1 }, { hlGroup: 'Search', lnum: 1, colStart: 0, colEnd: 3 }] let res = await window.diffHighlights(buf.id, ns, newItems) await window.applyDiffHighlights(buf.id, ns, priority, res) let markers = await buf.getExtMarks(ns_id, 0, -1, { details: true }) expect(markers.length).toBe(2) }) it('should ignore lines without highlights', async () => { let buf = await createFile() let items: HighlightItem[] = [{ hlGroup: 'Search', lnum: 1, colStart: 0, colEnd: 3 }] await setHighlights(items) let res = await window.diffHighlights(buf.id, ns, []) await window.applyDiffHighlights(buf.id, ns, priority, res) let markers = await buf.getExtMarks(ns_id, 0, -1, { details: true }) expect(markers.length).toBe(0) }) it('should return empty diff', async () => { let buf = await createFile() let items: HighlightItem[] = [{ hlGroup: 'Search', lnum: 0, colStart: 0, colEnd: 3 }] await setHighlights(items) let res = await window.diffHighlights(buf.id, ns, items) expect(res).toBeDefined() expect(res.remove).toEqual([]) expect(res.add).toEqual([]) expect(res.removeMarkers).toEqual([]) }) it('should remove and add highlights', async () => { let buf = await createFile() let items: HighlightItem[] = [{ hlGroup: 'Search', lnum: 0, colStart: 0, colEnd: 3 }] await setHighlights(items) items = [{ hlGroup: 'Search', lnum: 1, colStart: 0, colEnd: 3 }] let res = await window.diffHighlights(buf.id, ns, items) expect(res).toBeDefined() expect(res.add.length).toBe(1) expect(res.removeMarkers.length).toBe(1) await window.applyDiffHighlights(buf.id, ns, priority, res) let markers = await buf.getExtMarks(ns_id, 0, -1, { details: true }) expect(markers.length).toBe(1) expect(markers[0][1]).toBe(1) expect(markers[0][3].end_col).toBe(3) }) it('should update highlights of single line', async () => { let buf = await createFile() let items: HighlightItem[] = [{ hlGroup: 'Search', lnum: 0, colStart: 0, colEnd: 1 }, { hlGroup: 'Search', lnum: 1, colStart: 2, colEnd: 3 }] await setHighlights(items) items = [{ hlGroup: 'Search', lnum: 0, colStart: 2, colEnd: 3 }] let res = await window.diffHighlights(buf.id, ns, items) expect(res).toBeDefined() expect(res.add.length).toBe(1) expect(res.removeMarkers.length).toBe(2) await window.applyDiffHighlights(buf.id, ns, priority, res) let markers = await buf.getExtMarks(ns_id, 0, -1, { details: true }) expect(markers.length).toBe(1) expect(markers[0][1]).toBe(0) expect(markers[0][3].end_col).toBe(3) }) }) }) ================================================ FILE: src/__tests__/modules/workspace.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuid } from 'uuid' import { Disposable } from 'vscode-languageserver-protocol' import { Location, Position, Range, TextEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { userSettingsSchemaId } from '../../configuration' import events from '../../events' import { disposeAll } from '../../util' import workspace, { Workspace } from '../../workspace' import helper, { createTmpFile } from '../helper' let nvim: Neovim let disposables: Disposable[] = [] let tmpFolder = path.join(os.tmpdir(), `coc-${process.pid}`) beforeAll(async () => { await helper.setup() nvim = helper.nvim if (!fs.existsSync(tmpFolder)) fs.mkdirSync(tmpFolder) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { await helper.reset() disposeAll(disposables) disposables = [] }) describe('workspace properties', () => { it('should have initialized', async () => { let { nvim, uri, insertMode, workspaceFolder, cwd, documents, textDocuments } = workspace expect(insertMode).toBe(false) expect(nvim).toBeTruthy() expect(documents.length).toBe(1) expect(textDocuments.length).toBe(1) expect(cwd).toBe(process.cwd()) let floatSupported = workspace.floatSupported expect(floatSupported).toBe(true) let { pluginRoot } = workspace expect(typeof pluginRoot).toBe('string') let { isVim, isNvim } = workspace expect(isVim).toBe(false) expect(isNvim).toBe(true) expect(uri).toBeDefined() expect(workspaceFolder).toBeUndefined() let watchmanPath = workspace.getWatchmanPath() expect(watchmanPath == null || typeof watchmanPath === 'string').toBe(true) let folder = workspace.getWorkspaceFolder(URI.parse('lsp:/1')) expect(folder).toBeUndefined() let rootPath = await helper.doAction('currentWorkspacePath') expect(rootPath).toBe(process.cwd()) }) it('should get filetyps', async () => { await helper.edit('f.js') let filetypes = workspace.filetypes expect(filetypes.has('javascript')).toBe(true) let languageIds = workspace.languageIds expect(languageIds.has('javascript')).toBe(true) }) it('should get display width', () => { expect(workspace.getDisplayWidth('a')).toBe(1) }) it('should get channelNames', async () => { let names = workspace.channelNames expect(Array.isArray(names)).toBe(true) }) it('should work with deprecated method', async () => { await nvim.setLine('foo') await workspace['moveTo'](Position.create(0, 1)) let col = await nvim.call('col', ['.']) expect(col).toBe(2) }) }) describe('workspace methods', () => { it('should call vim method', async () => { let res = await workspace.callAsync('bufnr', ['%']) expect(typeof res).toBe('number') let obj: any = workspace.env obj.isVim = true disposables.push({ dispose: () => { obj.isVim = false } }) res = await workspace.callAsync('bufnr', ['%']) expect(typeof res).toBe('number') }) it('should get the document', async () => { let doc = await workspace.document let buf = await nvim.buffer expect(doc.buffer.equals(buf)).toBeTruthy() doc = workspace.getDocument(doc.uri) expect(doc.buffer.equals(buf)).toBeTruthy() }) it('should get uri', async () => { let doc = await workspace.document expect(workspace.getUri(doc.bufnr, undefined)).toBeDefined() expect(workspace.getUri(999, null)).toBeNull() expect(workspace.getUri(999)).toBe('') }) it('should fixWin32unixPrefix', async () => { expect(workspace.fixWin32unixFilepath('/foo')).toBe('/foo') }) it('should get attached document', async () => { let fn = () => { workspace.getAttachedDocument('file://not_exists') } expect(fn).toThrow(Error) await nvim.command(`edit +setl\\ buftype=nofile [tree]`) let doc = await workspace.document expect(doc.attached).toBe(false) fn = () => { workspace.getAttachedDocument(doc.bufnr) } expect(fn).toThrow(Error) }) it('should get format options of without bufnr', async () => { let opts = await workspace.getFormatOptions() expect(opts.insertSpaces).toBe(true) expect(opts.tabSize).toBe(2) }) it('should get format options of current buffer', async () => { let buf = await nvim.buffer await buf.setVar('coc_trim_trailing_whitespace', 1) await buf.setVar('coc_trim_final_newlines', 1) await buf.setOption('shiftwidth', 8) await buf.setOption('expandtab', false) let doc = workspace.getDocument(buf.id) let opts = await workspace.getFormatOptions(doc.uri) expect(opts).toEqual({ tabSize: 8, insertSpaces: false, insertFinalNewline: true, trimTrailingWhitespace: true, trimFinalNewlines: true }) }) it('should check document', async () => { let doc = await workspace.document expect(workspace.hasDocument(doc.uri)).toBe(true) expect(workspace.hasDocument(doc.uri, doc.version)).toBe(true) expect(workspace.hasDocument(doc.uri, doc.version - 1)).toBe(false) }) it('should get format options when uri does not exist', async () => { let uri = URI.file('/tmp/foo').toString() let opts = await workspace.getFormatOptions(uri) expect(opts.insertSpaces).toBe(true) expect(opts.tabSize).toBe(2) }) it('should create file watcher', async () => { let watcher = workspace.createFileSystemWatcher('**/*.ts') expect(watcher).toBeDefined() }) it('should get quickfix item from Location', async () => { let filepath = await createTmpFile('quickfix') let uri = URI.file(filepath).toString() let p = Position.create(0, 0) let loc = Location.create(uri, Range.create(p, p)) let item = await workspace.getQuickfixItem(loc) expect(item.filename).toBe(filepath) expect(item.text).toBe('quickfix') }) it('should get quickfix list from Locations', async () => { let filepathA = await createTmpFile('fileA:1\nfileA:2\nfileA:3') let uriA = URI.file(filepathA).toString() let filepathB = await createTmpFile('fileB:1\nfileB:2\nfileB:3') let uriB = URI.file(filepathB).toString() let p1 = Position.create(0, 0) let p2 = Position.create(1, 0) let locations: Location[] = [] locations.push(Location.create(uriA, Range.create(p1, p1))) locations.push(Location.create(uriA, Range.create(p2, p2))) locations.push(Location.create(uriB, Range.create(p1, p1))) locations.push(Location.create(uriB, Range.create(p2, p2))) let items = await workspace.getQuickfixList(locations) expect(items[0].filename).toBe(filepathA) expect(items[0].text).toBe('fileA:1') expect(items[1].filename).toBe(filepathA) expect(items[1].text).toBe('fileA:2') expect(items[2].filename).toBe(filepathB) expect(items[2].text).toBe('fileB:1') expect(items[3].filename).toBe(filepathB) expect(items[3].text).toBe('fileB:2') }) it('should get line of document', async () => { let doc = await workspace.document await nvim.setLine('abc') let line = await workspace.getLine(doc.uri, 0) expect(line).toBe('abc') }) it('should get line of file', async () => { let filepath = await createTmpFile('quickfix') let uri = URI.file(filepath).toString() let line = await workspace.getLine(uri, 0) expect(line).toBe('quickfix') }) it('should read content from buffer', async () => { let doc = await workspace.document await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'foo' }]) let line = await workspace.readFile(doc.uri) expect(line).toBe('foo\n') }) it('should read content from file', async () => { let filepath = await createTmpFile('content') let content = await workspace.readFile(URI.file(filepath).toString()) expect(content).toBe(content) }) it('should expand filepath', async () => { let home = os.homedir() let res = workspace.expand('~/$NODE_ENV/') expect(res.startsWith(home)).toBeTruthy() expect(res).toContain(process.env.NODE_ENV) res = workspace.expand('$HOME/$NODE_ENV/') expect(res.startsWith(home)).toBeTruthy() expect(res).toContain(process.env.NODE_ENV) }) it('should expand variables', async () => { expect(workspace.expand('${workspace}/foo')).toBe(`${workspace.root}/foo`) expect(workspace.expand('${env:NODE_ENV}')).toBe(process.env.NODE_ENV) expect(workspace.expand('${cwd}')).toBe(workspace.cwd) let folder = path.basename(workspace.root) expect(workspace.expand('${workspaceFolderBasename}')).toBe(folder) await helper.edit('bar.ts') expect(workspace.expand('${file}')).toContain('bar') expect(workspace.expand('${fileDirname}')).toBe(path.dirname(__dirname)) expect(workspace.expand('${fileExtname}')).toBe('.ts') expect(workspace.expand('${fileBasename}')).toBe('bar.ts') expect(workspace.expand('${fileBasenameNoExtension}')).toBe('bar') }) it('should run command', async () => { let res = await workspace.runCommand('ls', __dirname, 1000) expect(res).toMatch('workspace') res = await workspace.runCommand('ls') expect(res).toBeDefined() }) it('should export deprecated properties', async () => { expect(workspace.completeOpt).toBeDefined() expect(workspace.createNameSpace('name')).toBeDefined() expect(Workspace).toBeDefined() expect(workspace['onDidOpenTerminal']).toBeDefined() expect(workspace['onDidCloseTerminal']).toBeDefined() let spy = jest.spyOn(workspace.nvim, 'call').mockImplementation(() => { return null }) workspace.checkVersion(0) spy.mockRestore() }) it('should resolve module path if exists', async () => { let res = await workspace.resolveModule('bytes') res = await workspace.resolveModule('bytes') expect(res).toBeTruthy() }) it('should not resolve module if it does not exist', async () => { let res = await workspace.resolveModule('foo') res = await workspace.resolveModule('foo') expect(res).toBeFalsy() }) it('should return match score for document', async () => { let doc = await helper.createDocument('tmp.xml') expect(workspace.match(['xml'], doc.textDocument)).toBe(10) expect(workspace.match(['wxml'], doc.textDocument)).toBe(0) expect(workspace.match([{ language: 'xml' }], doc.textDocument)).toBe(10) expect(workspace.match([{ language: 'wxml' }], doc.textDocument)).toBe(0) expect(workspace.match([{ pattern: '**/*.xml' }], doc.textDocument)).toBe(5) expect(workspace.match([{ pattern: '**/*.html' }], doc.textDocument)).toBe(0) expect(workspace.match([{ scheme: 'file' }], doc.textDocument)).toBe(5) expect(workspace.match([{ scheme: 'term' }], doc.textDocument)).toBe(0) expect(workspace.match([{ language: 'xml' }, { scheme: 'file' }], doc.textDocument)).toBe(10) expect(workspace.match([{ language: 'xml', scheme: 'file', pattern: '**/*.xml' }], doc.textDocument)).toBe(10) }) it('should handle will save event', async () => { async function doRename() { let fsPath = await createTmpFile('foo', disposables) let newPath = path.join(path.dirname(fsPath), 'new_file') disposables.push(Disposable.create(() => { if (fs.existsSync(newPath)) fs.unlinkSync(newPath) })) await workspace.renameFile(fsPath, newPath, { overwrite: true }) if (fs.existsSync(newPath)) fs.unlinkSync(newPath) } let called = false let disposable = workspace.onWillRenameFiles(e => { let p = new Promise(resolve => { setTimeout(() => { called = true resolve() }, 10) }) e.waitUntil(p) }) await doRename() disposable.dispose() expect(called).toBe(true) called = false disposable = workspace.onWillRenameFiles(e => { called = true e.waitUntil(Promise.resolve({ changes: {} })) }) await doRename() expect(called).toBe(true) disposable.dispose() }) it('should getWatchConfig', async () => { helper.updateConfiguration('fileSystemWatch.enable', null, disposables) helper.updateConfiguration('fileSystemWatch.watchmanPath', '~/bin/watchman', disposables) helper.updateConfiguration('fileSystemWatch.ignoredFolders', ['~'], disposables) let config = workspace.getWatchConfig() expect(config.enable).toBe(false) expect(typeof config.watchmanPath).toBe('string') expect(config.ignoredFolders).toEqual([os.homedir()]) }) }) describe('workspace utility', () => { it('should create database', async () => { let filpath = path.join(process.env.COC_DATA_HOME, 'test.json') if (fs.existsSync(filpath)) { fs.unlinkSync(filpath) } let db = workspace.createDatabase('test') let res = db.exists('xyz') expect(res).toBe(false) db.destroy() }) it('should get current state', async () => { let buf = await helper.edit() await buf.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) await nvim.call('cursor', [2, 2]) let doc = workspace.getDocument(buf.id) let state = await workspace.getCurrentState() expect(doc.uri).toBe(state.document.uri) expect(state.position).toEqual({ line: 1, character: 1 }) }) it('should findUp to tsconfig.json from current file', async () => { await helper.edit(path.join(__dirname, 'edit')) let filepath = await workspace.findUp('tsconfig.json') expect(filepath).toMatch('tsconfig.json') }) it('should findUp from current file ', async () => { await helper.edit('foo') let filepath = await workspace.findUp('tsconfig.json') expect(filepath).toMatch('tsconfig.json') }) it('should not findUp from file in other directory', async () => { await nvim.command(`edit ${path.join(os.tmpdir(), uuid())}`) let filepath = await workspace.findUp('tsconfig.json') expect(filepath).toBeNull() }) it('should register autocmd', async () => { let event: any let eventCount = 0 let disposables = [] workspace.registerAutocmd({ event: 'TextYankPost', request: true, arglist: ['v:event'], callback: ev => { eventCount += 1 event = ev } }, disposables) await nvim.setLine('foo') await nvim.command('normal! yy') await helper.wait(30) expect(event.regtype).toBe('V') expect(event.operator).toBe('y') expect(event.regcontents).toEqual(['foo']) expect(eventCount).toBe(1) disposables.forEach(d => d.dispose()) }) it('should register keymap', async () => { let n = 0 let fn = () => { n++ } await nvim.command('nmap go (coc-echo)') let disposable = workspace.registerKeymap(['n', 'v'], 'echo', fn, { sync: true }) let { mode } = await nvim.mode expect(mode).toBe('n') await nvim.call('feedkeys', ['go', 'i']) await helper.waitValue(() => n, 1) disposable.dispose() await nvim.call('feedkeys', ['go', 'i']) await helper.wait(20) expect(n).toBe(1) }) it('should register expr keymap', async () => { let called = false let fn = () => { called = true return '""' } await nvim.input('i') let { mode } = await nvim.mode expect(mode).toBe('i') let disposable = workspace.registerExprKeymap('i', '"', fn) await helper.wait(30) await nvim.call('feedkeys', ['"', 't']) await helper.wait(30) expect(called).toBe(true) let line = await nvim.line expect(line).toBe('""') disposable.dispose() }) it('should register buffer expr keymap', async () => { let fn = () => '""' await nvim.input('i') let disposable = workspace.registerExprKeymap('i', '"', fn, true, false) await helper.wait(30) await nvim.call('feedkeys', ['"', 't']) await helper.wait(30) let line = await nvim.line expect(line).toBe('""') disposable.dispose() }) it('should check nvim version', async () => { expect(workspace.has('patch-7.4.248')).toBe(false) expect(workspace.has('nvim-0.5.0')).toBe(true) expect(workspace.has('nvim-9.0.0')).toBe(false) }) it('should registerLocalKeymap by old API', async () => { let called = false let fn = workspace.registerLocalKeymap.bind(workspace) as any let disposable = fn('n', 'n', () => { called = true }) await nvim.call('feedkeys', ['n', 't']) await helper.waitValue(() => called, true) disposable.dispose() let res = await nvim.exec('nmap n', true) expect(res).toMatch('No mapping found') }) }) describe('workspace events', () => { it('should listen to fileType change', async () => { let buf = await helper.edit() await nvim.command('setf xml') await helper.wait(50) let doc = workspace.getDocument(buf.id) expect(doc.filetype).toBe('xml') }) it('should fire onDidOpenTextDocument', async () => { let fn = jest.fn() workspace.onDidOpenTextDocument(fn, null, disposables) await helper.edit() await helper.wait(30) expect(fn).toHaveBeenCalledTimes(1) }) it('should fire onDidChangeTextDocument', async () => { let fn = jest.fn() await helper.edit() workspace.onDidChangeTextDocument(fn, null, disposables) await nvim.setLine('foo') let doc = await workspace.document doc.forceSync() await helper.wait(20) expect(fn).toHaveBeenCalledTimes(1) }) it('should fire onDidChangeConfiguration', async () => { let fn = jest.fn() let disposable = workspace.onDidChangeConfiguration(e => { disposable.dispose() expect(e.affectsConfiguration('tsserver')).toBe(true) expect(e.affectsConfiguration('tslint')).toBe(false) fn() }) let config = workspace.getConfiguration('tsserver') await config.update('enable', false) expect(fn).toHaveBeenCalledTimes(1) await config.update('enable', undefined) }) it('should resolve json schema', async () => { expect(workspace.resolveJSONSchema(userSettingsSchemaId)).toBeDefined() }) it('should get empty configuration for none exists section', () => { let config = workspace.getConfiguration('notexists') let keys = Object.keys(config) expect(keys.length).toBe(0) }) it('should fire onWillSaveUntil', async () => { let doc = await workspace.document let filepath = URI.parse(doc.uri).fsPath let fn = jest.fn() let disposable = workspace.onWillSaveTextDocument(event => { let promise = new Promise(resolve => { fn() let edit: TextEdit = { newText: 'foo', range: Range.create(0, 0, 0, 0) } resolve([edit]) }) event.waitUntil(promise) }) await nvim.setLine('bar') await helper.wait(30) await events.fire('BufWritePre', [doc.bufnr, doc.bufname]) await helper.wait(30) let content = doc.getDocumentContent() expect(content.startsWith('foobar')).toBe(true) disposable.dispose() expect(fn).toHaveBeenCalledTimes(1) if (fs.existsSync(filepath)) { fs.unlinkSync(filepath) } }) it('should not work for async waitUntil', async () => { let doc = await helper.createDocument() let filepath = URI.parse(doc.uri).fsPath let disposable = workspace.onWillSaveTextDocument(event => { setTimeout(() => { let edit: TextEdit = { newText: 'foo', range: Range.create(0, 0, 0, 0) } event.waitUntil(Promise.resolve([edit])) }, 30) }) await nvim.setLine('bar') await helper.wait(30) await nvim.command('wa') let content = doc.getDocumentContent() expect(content).toMatch('bar') disposable.dispose() if (fs.existsSync(filepath)) { fs.unlinkSync(filepath) } }) it('should only use first returned textEdits', async () => { let doc = await helper.createDocument() let filepath = URI.parse(doc.uri).fsPath disposables.push(Disposable.create(() => { if (fs.existsSync(filepath)) { fs.unlinkSync(filepath) } })) workspace.onWillSaveTextDocument(event => { event.waitUntil(Promise.resolve(undefined)) }, null, disposables) workspace.onWillSaveTextDocument(event => { let promise = new Promise(resolve => { setTimeout(() => { let edit: TextEdit = { newText: 'foo', range: Range.create(0, 0, 0, 0) } resolve([edit]) }, 10) }) event.waitUntil(promise) }, null, disposables) workspace.onWillSaveTextDocument(event => { let promise = new Promise(resolve => { setTimeout(() => { let edit: TextEdit = { newText: 'bar', range: Range.create(0, 0, 0, 0) } resolve([edit]) }, 30) }) event.waitUntil(promise) }, null, disposables) await nvim.setLine('bar') await helper.wait(30) await nvim.command('wa') let content = doc.getDocumentContent() expect(content).toMatch('foo') }) it('should attach & detach', async () => { let buf = await helper.edit() await nvim.command('CocDisable') let doc = workspace.getDocument(buf.id) expect(doc).toBeUndefined() await nvim.command('CocEnable') doc = workspace.getDocument(buf.id) expect(doc.bufnr).toBe(buf.id) }) }) describe('workspace registerBufferSync', () => { it('should register', async () => { await helper.createDocument() let created = 0 let deleted = 0 let changed = 0 let disposable = workspace.registerBufferSync(() => { created = created + 1 return { dispose: () => { deleted += 1 }, onChange: () => { changed += 1 } } }) disposables.push(disposable) let doc = await helper.createDocument() expect(created).toBe(2) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) expect(changed).toBe(1) await nvim.command('bd!') expect(deleted).toBe(1) }) it('should invoke onTextChange', async () => { let called = 0 disposables.push(workspace.registerBufferSync(() => { return { dispose: () => { }, onTextChange: () => { called = called + 1 } } })) let doc = await helper.createDocument() await nvim.setLine('foo') await doc.synchronize() expect(called).toBe(1) }) }) ================================================ FILE: src/__tests__/npm ================================================ #!/usr/bin/env node console.log('arguments' + JSON.stringify(process.argv)) console.error('error msg') console.log('start') console.log('finish') if (process.argv.includes('--error')) { process.exit(3) } ================================================ FILE: src/__tests__/rg ================================================ #!/usr/bin/env node // black:30 red:31 green:32 let content = `\x1b[30mmodules/cursors.test.ts\x1b[m \x1b[32m218\x1b[m- let doc = await setup() \x1b[32m219\x1b[m- await nvim.call('cursor', [1, 4]) \x1b[32m220\x1b[m: await nvim.input('\x1b[31mabc\x1b[m') \x1b[32m221\x1b[m- await helper.wait(30) \x1b[32m222\x1b[m- doc.forceSync() \x1b[32m223\x1b[m- await helper.wait(100) \x1b[32m224\x1b[m- let lines = await nvim.call('getline', [1, '$']) \x1b[32m225\x1b[m: expect(lines).toEqual(['\x1b[31mabc\x1b[m fooabc fooabc', 'barabc barabc']) \x1b[32m226\x1b[m- }) \x1b[32m227\x1b[m- -- \x1b[32m32\x1b[m- expect(rangeCount()).toBe(5) \x1b[32m33\x1b[m- let lines = await nvim.call('getline', [1, '$']) \x1b[32m34\x1b[m: expect(lines).toEqual(['\x1b[31mabc\x1b[m fooabc fooabc', 'barabc barabc']) \x1b[32m35\x1b[m- }) \x1b[32m36\x1b[m- \x1b[30mmodules/position.test.ts\x1b[m \x1b[32m42\x1b[m- test('getChangedPosition #1', () => { \x1b[32m43\x1b[m- let pos = Position.create(0, 0) \x1b[32m44\x1b[m: let edit = TextEdit.insert(pos, '\x1b[31mabc\x1b[m') \x1b[32m45\x1b[m- let res = getChangedPosition(pos, edit) \x1b[32m46\x1b[m- expect(res).toEqual({ line: 0, character: 3 }) ` let idx = process.argv.findIndex(s => s == '--sleep') if (idx !== -1) { let ms = process.argv[idx + 1] setTimeout(() => { process.stdout.write(content) }, ms) } else { process.stdout.write(content) } ================================================ FILE: src/__tests__/sample/.vim/coc-settings.json ================================================ { "coc.preferences.rootPath": "./src" } ================================================ FILE: src/__tests__/snippets/manager.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import path from 'path' import { CompletionItem, Disposable, InsertTextFormat, InsertTextMode, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import commandManager from '../../commands' import events from '../../events' import languages from '../../languages' import Document from '../../model/document' import { CompletionItemProvider } from '../../provider' import snippetManager, { SnippetManager } from '../../snippets/manager' import { SnippetEdit } from '../../snippets/session' import { SnippetString } from '../../snippets/string' import { disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let doc: Document let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim let pyfile = path.join(__dirname, '../ultisnips.py') await nvim.command(`execute 'pyxfile '.fnameescape('${pyfile}')`) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) beforeEach(async () => { doc = await helper.createDocument() }) describe('snippet provider', () => { describe('Events', () => { it('should change status item on editor change', async () => { let doc = await helper.createDocument('foo') await nvim.input('i') await snippetManager.insertSnippet('${1:foo} $1 ') let val = await nvim.getVar('coc_status') expect(val).toBeDefined() expect(snippetManager.isActivated(doc.bufnr)).toBe(true) await nvim.command('edit bar') await helper.waitValue(async () => { let val = await nvim.getVar('coc_status') as string return val.includes('SNIP') }, false) await nvim.command('buffer ' + doc.bufnr) await helper.waitValue(async () => { let val = await nvim.getVar('coc_status') as string return val.includes('SNIP') }, true) }) it('should check position on InsertEnter', async () => { await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) let isActive = await snippetManager.insertSnippet('${1:foo} $1 ', false, Range.create(0, 0, 0, 0)) expect(isActive).toBe(true) let line = await nvim.line await nvim.call('cursor', [1, line.length + 1]) await events.fire('InsertEnter', [doc.bufnr]) expect(snippetManager.session.isActive).toBe(false) }) it('should synchronize on CompleteDone', async () => { let doc = await workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foot\n')]) await nvim.call('cursor', [2, 1]) await nvim.command('startinsert') let res = await snippetManager.insertSnippet('${1/(.*)/${1:/capitalize}/}$1', true, Range.create(1, 0, 1, 0)) expect(res).toBe(true) await snippetManager.selectCurrentPlaceholder() await nvim.input('f') await helper.waitPopup() let line = await nvim.line expect(line).toBe('f') await nvim.input('t') let s = snippetManager.session await doc.patchChange() events.completing = false await s.onCompleteDone() line = await nvim.line expect(line).toBe('Ftft') await nvim.input('') await helper.waitValue(() => { return nvim.line }, 'Ff') }) it('should show & hide status item', async () => { let doc = await workspace.document let buf = doc.buffer let curr = await helper.createDocument() await buf.setLines([], { start: 0, end: -1 }) let isActive = await snippetManager.insertBufferSnippet(buf.id, ' ${1:foo} $1 $0', Range.create(0, 0, 0, 0)) expect(isActive).toBe(true) let status = await nvim.getVar('coc_status') expect(!!status).toBe(false) await doc.applyEdits([TextEdit.insert(Position.create(0, 1), 'x')]) await helper.waitValue(() => doc.getline(0), ' xfoo xfoo ') let active = await buf.getVar('coc_snippet_active') expect(active).toBe(1) active = await curr.buffer.getVar('coc_snippet_active') expect(active != 1).toBe(true) }) }) describe('insertSnippet()', () => { it('should throw when current buffer not attached', async () => { await nvim.command(`vnew +setl\\ buftype=nofile`) await expect(async () => { await snippetManager.insertSnippet('foo') }).rejects.toThrow(Error) }) it('should replace range for ultisnip with python code', async () => { await nvim.setLine('foo') await snippetManager.insertSnippet('`!p snip.rv = vim.current.line`', false, Range.create(0, 0, 0, 3), InsertTextMode.asIs, {}) let line = await nvim.line expect(line).toBe('') await helper.doAction('selectCurrentPlaceholder') }) it('should not active when insert plain snippet', async () => { await snippetManager.insertSnippet('foo') let line = await nvim.line expect(line).toBe('foo') expect(snippetManager.session.isActive).toBe(false) expect(snippetManager.getSession(doc.bufnr)).toBeUndefined() }) it('should insert snippet by action', async () => { await nvim.input('i') let res = await helper.plugin.cocAction('snippetInsert', Range.create(0, 0, 0, 0), '${1:foo}') expect(res).toBe(true) }) it('should start new session if session exists', async () => { await nvim.setLine('bar') await snippetManager.insertSnippet('${1:foo} ') await nvim.input('') await nvim.command('stopinsert') await nvim.input('A') let s = new SnippetString() s.appendPlaceholder('bar') let active = await snippetManager.insertSnippet(s) expect(active).toBe(true) let line = await nvim.getLine() expect(line).toBe('foo barbar') }) it('should start nest session', async () => { await snippetManager.insertSnippet('${1:foo} ${2:bar}', true, Range.create(0, 0, 0, 0), InsertTextMode.asIs, {}) await nvim.input('i') let s = snippetManager.session await s.forceSynchronize() let active = await snippetManager.insertSnippet('${1:x} $1', true, undefined, undefined, { actions: { preExpand: 'vim.vars["last"] = snip.last_placeholder.current_text' } }) expect(active).toBe(true) let last = await nvim.getVar('last') expect(last).toBe('i') }) it('should insert nested snippet on CompleteDone with correct position', async () => { await snippetManager.insertSnippet('`!p snip.rv = " " * (10 - len(t[1]))`${1:inner}', true, Range.create(0, 0, 0, 0), InsertTextMode.asIs, {}) let bufnr = await nvim.call('bufnr', ['%']) as number let session = snippetManager.getSession(bufnr) expect(session.isActive).toBe(true) let line = await nvim.line expect(line).toBe(' inner') let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'bar', insertTextFormat: InsertTextFormat.Snippet, textEdit: { range: Range.create(0, 5, 0, 6), newText: '${1:foobar}' }, preselect: true }] } disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) await nvim.input('b') await helper.waitPopup() let res = await helper.items() let idx = res.findIndex(o => o.source?.name == 'edits') nvim.call('coc#pum#select', [idx, 1, 1], true) await events.race(['PlaceholderJump'], 200) await session.synchronize() line = await nvim.line expect(line).toBe(' foobar') }) }) describe('insertBufferSnippet()', () => { it('should throw when buffer not attached', async () => { await nvim.command(`vnew +setl\\ buftype=nofile`) let bufnr = await nvim.call('bufnr', ['%']) as number expect(snippetManager.jumpable()).toBe(false) let res = await snippetManager.resolveSnippet('${1:foo}') expect(res).toBeUndefined() await expect(async () => { await snippetManager.insertBufferSnippet(bufnr, 'foo', Range.create(0, 0, 0, 0)) }).rejects.toThrow(Error) }) }) describe('insertBufferSnippets()', () => { it('should insert snippets', async () => { let doc = await helper.createDocument() await helper.createDocument() let edits: SnippetEdit[] = [] edits.push({ range: Range.create(0, 0, 0, 0), snippet: 'foo($1)' }) edits.push({ range: Range.create(0, 0, 0, 0), snippet: 'bar($1)' }) let result = await snippetManager.insertBufferSnippets(doc.bufnr, edits) expect(result).toBe(true) let lines = await doc.buffer.lines expect(lines).toEqual(['foo()bar()']) await nvim.command(`b ${doc.bufnr}`) // selected on BufEnter await helper.waitFor('col', ['.'], 5) }) it('should select placeholder', async () => { let doc = await workspace.document let edits: SnippetEdit[] = [] edits.push({ range: Range.create(0, 0, 0, 0), snippet: 'foo($1)' }) edits.push({ range: Range.create(0, 0, 0, 0), snippet: 'bar($1)' }) let result = await snippetManager.insertBufferSnippets(doc.bufnr, edits, true) expect(result).toBe(true) let cursor = await window.getCursorPosition() expect(cursor).toEqual(Position.create(0, 4)) }) }) describe('nextPlaceholder()', () => { it('should go to next placeholder', async () => { await snippetManager.insertSnippet('${1:a} ${2:b}') await helper.doAction('snippetNext') let col = await nvim.call('col', '.') expect(col).toBe(3) }) it('should remove keymap on nextPlaceholder when session not exists', async () => { await nvim.command(`edit +setl\\ buftype=nofile foo`) await events.fire('Enter', []) let buf = await nvim.buffer await nvim.call('coc#snippet#enable') await snippetManager.nextPlaceholder() let val = await buf.getVar('coc_snippet_active') expect(val).toBe(0) }) it('should respect preferCompleteThanJumpPlaceholder', async () => { helper.updateConfiguration('suggest.preferCompleteThanJumpPlaceholder', true, disposables) let provider: CompletionItemProvider = { provideCompletionItems: async (): Promise => [{ label: 'foot', insertTextFormat: InsertTextFormat.Snippet, insertText: '${1:foot}', textEdit: { range: Range.create(0, 0, 0, 0), newText: '${1:foot}' }, preselect: true }] } disposables.push(languages.registerCompletionItemProvider('edits', 'E', ['*'], provider)) await snippetManager.insertSnippet('${1} ${2:bar} foot') let mode = await nvim.mode expect(mode.mode).toBe('i') nvim.call('coc#start', { source: 'edits' }, true) await helper.waitPopup() await nvim.input('') await helper.waitFor('getline', ['.'], 'foot bar foot') let placeholder = snippetManager.session.placeholder expect(placeholder.index).toBe(1) }) }) describe('previousPlaceholder()', () => { it('should goto previous placeholder', async () => { await snippetManager.insertSnippet('${1:a} ${2:b}') await snippetManager.nextPlaceholder() await helper.doAction('snippetPrev') let col = await nvim.call('col', '.') expect(col).toBe(1) }) it('should remove keymap on previousPlaceholder when session not exists', async () => { await nvim.command(`edit +setl\\ buftype=nofile foo`) let buf = await nvim.buffer await nvim.call('coc#snippet#enable') await snippetManager.previousPlaceholder() let val = await buf.getVar('coc_snippet_active') expect(val).toBe(0) }) }) describe('cancel()', () => { it('should cancel snippet session', async () => { let buffer = doc.buffer let active = await snippetManager.insertSnippet('${1:foo}') expect(active).toBe(true) await helper.doAction('snippetCancel') expect(snippetManager.session.isActive).toBe(false) let val = await buffer.getVar('coc_snippet_active') expect(val).toBe(0) }) }) describe('jumpable()', () => { it('should check jumpable', async () => { await nvim.input('i') await snippetManager.insertSnippet('${1:foo} ${2:bar}') let jumpable = snippetManager.jumpable() expect(jumpable).toBe(true) await snippetManager.nextPlaceholder() jumpable = snippetManager.jumpable() expect(jumpable).toBe(true) await snippetManager.nextPlaceholder() jumpable = snippetManager.jumpable() expect(jumpable).toBe(false) }) }) describe('synchronize text', () => { it('should update placeholder on placeholder update', async () => { let doc = await workspace.document await nvim.command('startinsert') await snippetManager.insertSnippet('$1\n${1/,/|/g}', true, undefined, InsertTextMode.adjustIndentation, {}) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'a,b')]) let s = snippetManager.getSession(doc.bufnr) await s.forceSynchronize() let lines = await nvim.call('getline', [1, '$']) expect(lines).toEqual(['a,b', 'a|b']) }) it('should synchronize when position changed and pum visible', async () => { let doc = await workspace.document await nvim.setLine('foo') await nvim.input('o') let res = await snippetManager.insertSnippet("`!p snip.rv = ' '*(4- len(t[1]))`${1}", true, undefined, InsertTextMode.asIs, {}) expect(res).toBe(true) let line = await nvim.line expect(line).toBe(' ') await nvim.input('f') await helper.waitFor('coc#pum#visible', [], 1) await nvim.input('') let s = snippetManager.getSession(doc.bufnr) expect(s).toBeDefined() }) it('should adjust cursor position on update', async () => { await nvim.call('cursor', [1, 1]) await nvim.input('i') await snippetManager.insertSnippet('${1/..*/ -> /}$1') let line = await nvim.line expect(line).toBe('') await nvim.input('x') let s = snippetManager.getSession(doc.bufnr) expect(s).toBeDefined() await s.forceSynchronize() line = await nvim.line expect(line).toBe(' -> x') let col = await nvim.call('col', '.') expect(col).toBe(6) }) it('should not synchronize text on change final placeholder', async () => { let doc = await workspace.document await nvim.input('i') let res = await snippetManager.insertSnippet('$0e$1mpty$0') expect(res).toBe(true) await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 0, ['abc']]) await doc.synchronize() let s = snippetManager.getSession(doc.bufnr) await s.forceSynchronize() let line = await nvim.line expect(line).toBe('abcempty') }) }) describe('resolveSnippet()', () => { it('should resolve snippet text', async () => { let snippet = await snippetManager.resolveSnippet('${1:foo}') expect(snippet.toString()).toBe('foo') snippet = await snippetManager.resolveSnippet('${1:foo} ${2:`!p snip.rv = "foo"`}', {}) expect(snippet.toString()).toBe('foo foo') }) it('should resolve python when have python snippet', async () => { await nvim.command('startinsert') let res = await snippetManager.insertSnippet('${1:foo} `!p snip.rv = t[1]`', true, Range.create(0, 0, 0, 0), InsertTextMode.asIs, {}) as any expect(res).toBe(true) let snippet = await snippetManager.resolveSnippet('${1:x} `!p snip.rv= t[1]`', {}) expect(snippet.toString()).toBe('x x') }) it('should throw when resolve throw error', async () => { let s = snippetManager.session let spy = jest.spyOn(s, 'resolveSnippet').mockImplementation(() => { throw new Error('custom error') }) await expect(() => { return snippetManager.resolveSnippet('${1:x}') }).rejects.toThrow(Error) spy.mockRestore() }) }) describe('normalizeInsertText()', () => { it('should normalizeInsertText', async () => { let doc = await workspace.document let res = await snippetManager.normalizeInsertText(doc.bufnr, 'foo\nbar', ' ', InsertTextMode.asIs) expect(res).toBe('foo\nbar') }) it('should respect noExpand', async () => { await nvim.command('startinsert') let res = await snippetManager.insertSnippet('\t\t${1:foo}', true, Range.create(0, 0, 0, 0), InsertTextMode.adjustIndentation, { noExpand: true }) expect(res).toBe(true) let line = await nvim.line expect(line).toBe('\t\tfoo') }) }) describe('insertSnippet command', () => { it('should insert ultisnips snippet', async () => { expect(SnippetManager).toBeDefined() await nvim.setLine('foo') let edit = TextEdit.replace(Range.create(0, 0, 0, 3), '${1:`echo "bar"`}') await commandManager.executeCommand('editor.action.insertSnippet', edit, {}) let line = await nvim.line expect(line).toBe('bar') edit = TextEdit.replace(Range.create(0, 0, 0, 3), '${1:`echo "foo"`}') await commandManager.executeCommand('editor.action.insertSnippet', edit, { regex: '' }) line = await nvim.line expect(line).toBe('foo') }) }) describe('Snippet context and actions', () => { describe('context', () => { it('should insert context snippet', async () => { await nvim.setLine('prefix') await nvim.input('A') let isActive = await snippetManager.insertSnippet('pre${1:foo} $0', true, undefined, undefined, { range: Range.create(0, 0, 0, 6), context: `True;vim.vars['before'] = snip.before` }) expect(isActive).toBe(true) let before = await nvim.getVar('before') expect(before).toBe('prefix') }) }) describe('pre_expand', () => { it('should insert with pre_expand and user set cursor', async () => { await nvim.command('normal! gg') await nvim.setLine('foo') await nvim.input('A') await snippetManager.insertSnippet('$1 ${2:bar}', true, Range.create(0, 0, 0, 3), undefined, { actions: { preExpand: "snip.buffer[snip.line] = ' '*4; snip.cursor.set(snip.line, 4)" } }) let line = await nvim.line expect(line).toBe(' bar') let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 4 }) snippetManager.cancel() }) it('should move to end of file with pre_expand', async () => { let buf = await nvim.buffer await buf.setLines(['x', 'foo'], { start: 0, end: 0 }) await nvim.command('normal! gg') await nvim.input('A') await snippetManager.insertSnippet('def $1():', true, Range.create(0, 0, 0, 1), undefined, { actions: { preExpand: "del snip.buffer[snip.line]; snip.buffer.append(''); snip.cursor.set(len(snip.buffer)-1, 0)" } }) let lines = await buf.lines expect(lines).toEqual(['foo', '', 'def ():']) let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 2, character: 4 }) }) it('should insert line before with pre_expand', async () => { let buf = await nvim.buffer await nvim.setLine('foo') await nvim.command('normal! gg') await nvim.input('A') await snippetManager.insertSnippet('pre$1():', true, Range.create(0, 0, 0, 3), undefined, { actions: { preExpand: "snip.buffer[snip.line:snip.line] = [''];" } }) let lines = await buf.lines expect(lines).toEqual(['', 'pre():']) let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 1, character: 3 }) }) it('should insert snippetwith pre_expand as nested python snippet', async () => { await snippetManager.insertSnippet('`!p snip.rv = " " * (10 - len(t[1]))`${1:inner}', true, Range.create(0, 0, 0, 0), InsertTextMode.asIs, {}) await nvim.setVar('coc_selected_text', 'bar') await snippetManager.insertSnippet('${1:foo}', true, Range.create(0, 5, 0, 10), undefined, { actions: { preExpand: 'vim.vars["v"] = snip.visual_content' } }) let line = await nvim.line expect(line).toBe(' foo') let res = await nvim.getVar('v') expect(res).toBe('bar') let val = await nvim.getVar('coc_selected_text') expect(val).toBeNull() }) }) describe('post_expand', () => { it('should change snippet_start and snippet_end on lines change', async () => { let buf = await nvim.buffer await nvim.input('i') let codes = [ "snip.buffer[0:0] = ['', '']", "vim.vars['first'] = [snip.snippet_start[0],snip.snippet_start[1],snip.snippet_end[0],snip.snippet_end[1]]", "snip.buffer[0:1] = []", "vim.vars['second'] = [snip.snippet_start[0],snip.snippet_start[1],snip.snippet_end[0],snip.snippet_end[1]]", ] let activated = await snippetManager.insertSnippet('pre$1():', true, Range.create(0, 0, 0, 0), undefined, { actions: { postExpand: codes.join(';') } }) expect(activated).toBe(true) let first = await nvim.getVar('first') expect(first).toEqual([2, 0, 2, 6]) let second = await nvim.getVar('second') expect(second).toEqual([1, 0, 1, 6]) let lines = await buf.lines expect(lines).toEqual(['', 'pre():']) }) it('should allow change after snippet', async () => { await nvim.input('i') let buf = await nvim.buffer // add two new lines let codes = [ "snip.buffer[snip.snippet_end[0]+1:snip.snippet_end[0]+1] = ['', '']", ] await snippetManager.insertSnippet('def $1()', true, Range.create(0, 0, 0, 0), undefined, { actions: { postExpand: codes.join(';') } }) let session = snippetManager.getSession(buf.id) expect(session.isActive).toBe(true) let lines = await buf.lines expect(lines).toEqual(['def ()', '', '']) }) }) describe('post_jump', () => { it('should insert before snippet', async () => { let buf = await nvim.buffer await nvim.input('i') let line = await nvim.call('line', ['.']) as number let codes = [ 'if snip.tabstop == 2: snip.buffer[0:0] = ["aa", "bb"];vim.vars["positions"] = [snip.snippet_start[0], snip.snippet_end[0]];vim.vars["direction"] = snip.jump_direction;', ] let activated = await snippetManager.insertSnippet('${1:foo} ${2:bar} $0', true, Range.create(line - 1, 0, line - 1, 0), undefined, { actions: { postJump: codes.join(';') } }) expect(activated).toBe(true) await snippetManager.nextPlaceholder() await events.race(['PlaceholderJump'], 500) let lines = await buf.lines expect(lines).toEqual(['aa', 'bb', 'foo bar ']) let positions = await nvim.getVar('positions') expect(positions).toEqual([2, 2]) await snippetManager.previousPlaceholder() }) it('should pass variables to snip', async () => { await nvim.input('o') let codes = [ "vim.vars['positions'] = [snip.snippet_start[0],snip.snippet_start[1],snip.snippet_end[0],snip.snippet_end[1]]", "vim.vars['tabstop'] = snip.tabstop", "vim.vars['jump_direction'] = snip.jump_direction", "vim.vars['tabstops'] = str(snip.tabstops)", ] let activated = await snippetManager.insertSnippet('${1:foo} ${2:测试} $0', true, Range.create(1, 0, 1, 0), undefined, { actions: { postJump: codes.join(';') } }) expect(activated).toBe(true) await events.race(['PlaceholderJump'], 200) let positions = await nvim.getVar('positions') expect(positions).toEqual([1, 0, 1, 7]) let tabstop = await nvim.getVar('tabstop') expect(tabstop).toBe(1) let dir = await nvim.getVar('jump_direction') expect(dir).toBe(1) let tabstops = await nvim.getVar('tabstops') expect(tabstops).toMatch('测试') await snippetManager.nextPlaceholder() await snippetManager.previousPlaceholder() }) }) }) describe('dispose()', () => { it('should dispose', async () => { let active = await snippetManager.insertSnippet('${1:foo}') expect(active).toBe(true) snippetManager.dispose() expect(snippetManager.session).toBeUndefined() }) }) }) ================================================ FILE: src/__tests__/snippets/parser.test.ts ================================================ /* eslint-disable */ import * as assert from 'assert' import { EvalKind } from '../../snippets/eval' import { Choice, CodeBlock, ConditionMarker, ConditionString, FormatString, getPlaceholderId, Marker, mergeTexts, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, transformEscapes, Variable } from '../../snippets/parser' describe('SnippetParser', () => { test('transformEscapes', () => { assert.equal(transformEscapes('b\\uabc\\LDef'), 'bAbcdef') assert.equal(transformEscapes('b\\lAbc\\LDef'), 'babcdef') assert.equal(transformEscapes('b\\Uabc\\Edef'), 'bABCdef') assert.equal(transformEscapes('b\\LABC\\Edef'), 'babcdef') assert.equal(transformEscapes(' \\n \\t'), ' \n \t') }) test('Empty Marker', () => { assert.ok(Marker != null) assert.strictEqual((new Text('')).snippet, undefined) }) test('Scanner', () => { const scanner = new Scanner() assert.equal(scanner.next().type, TokenType.EOF) scanner.text('abc') assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('{{abc}}') assert.equal(scanner.next().type, TokenType.CurlyOpen) assert.equal(scanner.next().type, TokenType.CurlyOpen) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.CurlyClose) assert.equal(scanner.next().type, TokenType.CurlyClose) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('abc() ') assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.OpenParen) assert.equal(scanner.next().type, TokenType.CloseParen) assert.equal(scanner.next().type, TokenType.Format) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('abc 123') assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.Format) assert.equal(scanner.next().type, TokenType.Int) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('$foo') assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('$foo_bar') assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('$foo-bar') assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.Dash) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('${foo}') assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.CurlyOpen) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.CurlyClose) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('${1223:foo}') assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.CurlyOpen) assert.equal(scanner.next().type, TokenType.Int) assert.equal(scanner.next().type, TokenType.Colon) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.CurlyClose) assert.equal(scanner.next().type, TokenType.EOF) scanner.text('\\${}') assert.equal(scanner.next().type, TokenType.Backslash) assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.CurlyOpen) assert.equal(scanner.next().type, TokenType.CurlyClose) scanner.text('${foo/regex/format/option}') assert.equal(scanner.next().type, TokenType.Dollar) assert.equal(scanner.next().type, TokenType.CurlyOpen) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.Forwardslash) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.Forwardslash) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.Forwardslash) assert.equal(scanner.next().type, TokenType.VariableName) assert.equal(scanner.next().type, TokenType.CurlyClose) assert.equal(scanner.next().type, TokenType.EOF) }) function assertText(value: string, expected: string, ultisnip = false) { const p = new SnippetParser(ultisnip) const actual = p.text(value) assert.equal(actual, expected) } function assertMarker(input: TextmateSnippet | Marker[] | string, ...ctors: Function[]) { let marker: Marker[] if (input instanceof TextmateSnippet) { marker = input.children } else if (typeof input === 'string') { const p = new SnippetParser() marker = p.parse(input).children } else { marker = input } while (marker.length > 0) { let m = marker.pop() let ctor = ctors.pop() assert.ok(m instanceof ctor) } assert.equal(marker.length, ctors.length) assert.equal(marker.length, 0) } function assertTextAndMarker(value: string, escaped: string, ...ctors: Function[]) { assertText(value, escaped) assertMarker(value, ...ctors) } function assertEscaped(value: string, expected: string) { const actual = SnippetParser.escape(value) assert.equal(actual, expected) } test('Parser, escaped', function() { assertEscaped('foo$0', 'foo\\$0') assertEscaped('foo\\$0', 'foo\\\\\\$0') assertEscaped('f$1oo$0', 'f\\$1oo\\$0') assertEscaped('${1:foo}$0', '\\${1:foo\\}\\$0') assertEscaped('$', '\\$') }) test('Parser, escaped ultisnips', () => { const actual = new SnippetParser(true).text('t\\`a\\`\n\\$ \\{\\}') expect(actual).toBe('t`a`\n$ {}') }) test('Parser, transform with empty placeholder', () => { const actual = new SnippetParser(true).text('${1} ${1/^(.*)/$1aa/}') expect(actual).toBe(' aa') }) test('Parser, isPlainText()', function() { const s = (input: string, res: boolean) => { assert.equal(SnippetParser.isPlainText(input), res) } s('abc', true) s('abc$0', true) s('ab$0chh', false) s('ab$1chh', false) }) test('Parser, paried curly brace in placeholder', () => { const getText = (text): string => { const parser = new SnippetParser(false) let snip = parser.parse(text) let res: Text snip.walk(marker => { if (marker instanceof Text) { res = marker } return true }) return res ? res.value : undefined } let text = getText('${1:{foo}}') expect(text).toBe('{foo}') text = getText('${1:ab{foo}}') expect(text).toBe('ab{foo}') text = getText('${1:ab{foo}cd}') expect(text).toBe('ab{foo}cd') }) test('Parser, first placeholder / variable', function() { const first = (input: string): Marker => { const p = new SnippetParser(false) let s = p.parse(input, true) return s.first } const assertPlaceholder = (m: any, index: number) => { assert.equal(m instanceof Placeholder, true) assert.equal(m.index, index) } assertPlaceholder(first('foo'), 0) assertPlaceholder(first('${1:foo}'), 1) assertPlaceholder(first('${2:foo}'), 2) const p = new SnippetParser(false) let s = p.parse('${1/from/to/}', true) let placeholder = s.placeholders[0] assert.strictEqual(placeholder.toTextmateString(), '${1/from/to/}') }) test('Parser, text', () => { assertText('$', '$') assertText('\\\\$', '\\$') assertText('{', '{') assertText('\\}', '}') assertText('\\abc', '\\abc') assertText('foo${f:\\}}bar', 'foo}bar') assertText('\\{', '\\{') assertText('I need \\\\\\$', 'I need \\$') assertText('\\', '\\') assertText('\\{{', '\\{{') assertText('{{', '{{') assertText('{{dd', '{{dd') assertText('}}', '}}') assertText('ff}}', 'ff}}') assertText('${foo/.*/complex${1:/upcase/i}', '${foo/.*/complex/upcase/i') assertText('${foo/.*/${1/upcase}', '${foo/.*/${1/upcase}') assertText('${VISUAL/.*/complex${1:/upcase}/i}', '${VISUAL/.*/complex/upcase/i}', true) assertText('${foo/.*/complex${p:/upcase}/i}', '${foo/.*/complex/upcase/i}') assertText('farboo', 'farboo') assertText('far{{}}boo', 'far{{}}boo') assertText('far{{123}}boo', 'far{{123}}boo') assertText('far\\{{123}}boo', 'far\\{{123}}boo') assertText('far{{id:bern}}boo', 'far{{id:bern}}boo') assertText('far{{id:bern {{basel}}}}boo', 'far{{id:bern {{basel}}}}boo') assertText('far{{id:bern {{id:basel}}}}boo', 'far{{id:bern {{id:basel}}}}boo') assertText('far{{id:bern {{id2:basel}}}}boo', 'far{{id:bern {{id2:basel}}}}boo') }) test('Parser ConditionMarker', () => { { let m = new ConditionMarker(1, [new Text('a '), new FormatString(1)], [new Text('b '), new FormatString(2)], ) let val = m.resolve('', ['', 'foo', 'bar']) expect(val).toBe('b bar') val = m.resolve('x', ['', 'foo', 'bar']) expect(val).toBe('a foo') m.addIfMarker(new Text('if')) m.addElseMarker(new Text('else')) let s = m.toTextmateString() expect(s).toBe('(?1:a ${1}if:b ${2}else)') expect(m.clone()).toBeDefined() } { let m = new ConditionMarker(1, [new Text('foo')], [] ) let text = m.toTextmateString() expect(text).toBe('(?1:foo)') } }) test('Parser, TM text', () => { assertTextAndMarker('foo${1:bar}}', 'foobar}', Text, Placeholder, Text) assertTextAndMarker('foo${1:bar}${2:foo}}', 'foobarfoo}', Text, Placeholder, Placeholder, Text) assertTextAndMarker('foo${1:bar\\}${2:foo}}', 'foobar}foo', Text, Placeholder) let [, placeholder] = new SnippetParser().parse('foo${1:bar\\}${2:foo}}').children let { children } = (placeholder) assert.equal((placeholder).index, '1') assert.ok(children[0] instanceof Text) assert.equal(children[0].toString(), 'bar}') assert.ok(children[1] instanceof Placeholder) assert.equal(children[1].toString(), 'foo') }) test('Parser, placeholder', () => { assertTextAndMarker('farboo', 'farboo', Text) assertTextAndMarker('far{{}}boo', 'far{{}}boo', Text) assertTextAndMarker('far{{123}}boo', 'far{{123}}boo', Text) assertTextAndMarker('far\\{{123}}boo', 'far\\{{123}}boo', Text) }) test('Parser, literal code', () => { assertTextAndMarker('far`123`boo', 'far`123`boo', Text) assertTextAndMarker('far\\`123\\`boo', 'far\\`123\\`boo', Text) }) test('Parser, variables/tabstop', () => { assertTextAndMarker('$far-boo', '-boo', Variable, Text) assertTextAndMarker('\\$far-boo', '$far-boo', Text) assertTextAndMarker('far$farboo', 'far', Text, Variable) assertTextAndMarker('far${farboo}', 'far', Text, Variable) assertTextAndMarker('$123', '', Placeholder) assertTextAndMarker('$farboo', '', Variable) assertTextAndMarker('$far12boo', '', Variable) assertTextAndMarker('000_${far}_000', '000__000', Text, Variable, Text) assertTextAndMarker('FFF_${TM_SELECTED_TEXT}_FFF$0', 'FFF__FFF', Text, Variable, Text, Placeholder) }) test('Parser, variables/placeholder with defaults', () => { assertTextAndMarker('${name:value}', 'value', Variable) assertTextAndMarker('${1:value}', 'value', Placeholder) assertTextAndMarker('${1:bar${2:foo}bar}', 'barfoobar', Placeholder) assertTextAndMarker('${name:value', '${name:value', Text) assertTextAndMarker('${1:bar${2:foobar}', '${1:barfoobar', Text, Placeholder) }) test('Parser, variable transforms', function() { assertTextAndMarker('${foo///}', '', Variable) assertTextAndMarker('${foo/regex/format/gmi}', '', Variable) assertTextAndMarker('${foo/([A-Z][a-z])/format/}', '', Variable) // invalid regex assertTextAndMarker('${foo/([A-Z][a-z])/format/GMI}', '${foo/([A-Z][a-z])/format/GMI}', Text) assertTextAndMarker('${foo/([A-Z][a-z])/format/funky}', '${foo/([A-Z][a-z])/format/funky}', Text) assertTextAndMarker('${foo/([A-Z][a-z]/format/}', '${foo/([A-Z][a-z]/format/}', Text) // tricky regex assertTextAndMarker('${foo/m\\/atch/$1/i}', '', Variable) assertMarker('${foo/regex\/format/options}', Text) // incomplete assertTextAndMarker('${foo///', '${foo///', Text) assertTextAndMarker('${foo/regex/format/options', '${foo/regex/format/options', Text) // format string assertMarker('${foo/.*/${0:fooo}/i}', Variable) assertMarker('${foo/.*/${1}/i}', Variable) assertMarker('${foo/.*/$1/i}', Variable) assertMarker('${foo/.*/This-$1-encloses/i}', Variable) assertMarker('${foo/.*/complex${1:else}/i}', Variable) assertMarker('${foo/.*/complex${1:-else}/i}', Variable) assertMarker('${foo/.*/complex${1:+if}/i}', Variable) assertMarker('${foo/.*/complex${1:?if:else}/i}', Variable) assertMarker('${foo/.*/complex${1:/upcase}/i}', Variable) }) test('Parse, parse code block', () => { assertText('aa \\`echo\\`', 'aa `echo`', true) assertText('aa `xyz`', 'aa ', true) assertText('aa `!v xyz`', 'aa ', true) assertText('aa `!p xyz`', 'aa ', true) assertText('aa `!p foo\nbar`', 'aa ', true) assertText('aa `!p py', 'aa `!p py', true) assertText('aa `!p \n`', 'aa ', true) assertText('aa `!p\n 1\n 2`', 'aa ', true) const c = text => { return (new SnippetParser(true)).parse(text) } assertMarker(c('`foo`'), CodeBlock) assertMarker(c('`!v bar`'), CodeBlock) assertMarker(c('`!p python`'), CodeBlock) const assertPlaceholder = (text: string, kind: EvalKind, code: string) => { let p = c(text).children[0] assert.ok(p instanceof Placeholder) let m = p.children[0] as CodeBlock assert.ok(m instanceof CodeBlock) assert.equal(m.kind, kind) assert.equal(m.code, code) } assertPlaceholder('${1:` foo `}', 'shell', 'foo') assertPlaceholder('${1:`!v bar`}', 'vim', 'bar') assertPlaceholder('${1:`!p python`}', 'python', 'python') assertPlaceholder('${1:`!p x\\`y`}', 'python', 'x\\`y') assertPlaceholder('${1:`!p x\ny`}', 'python', 'x\ny') assertPlaceholder('${1:`!p \nx\ny`}', 'python', 'x\ny') }) test('Parser, CodeBlock toTextmateString', () => { const c = text => { return (new SnippetParser(true)).parse(text) } expect(c('`foo`').toTextmateString()).toBe('`foo`') expect(c('`!p snip.rv`').toTextmateString()).toBe('`!p snip.rv`') expect(c('`!v "var"`').toTextmateString()).toBe('`!v "var"`') }) test('Parser, placeholder with CodeBlock primary', () => { const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${1/^_(.*)/$1/} $1 aa ${1:`!p snip.rv = "_foo"`}') let arr = s.placeholders arr = arr.filter(o => o.index == 1) assert.equal(arr.length, 3) let filtered = arr.filter(o => o.primary === true) assert.equal(filtered.length, 1) assert.equal(filtered[0], arr[2]) let children = arr.map(o => o.children[0]) assert.ok(children[0] instanceof Text) assert.ok(children[1] instanceof Text) assert.ok(children[2] instanceof CodeBlock) }) test('Parser, placeholder with CodeBlock not primary', () => { const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${1/^_(.*)/$1/} ${1:_foo} ${2:bar} $1 $3 ${1:`!p snip.rv = "three"`}') let arr = s.placeholders arr = arr.filter(o => o.index == 1) assert.equal(arr.length, 4) assert.ok(arr[0].transform) assert.equal(arr[1].primary, true) assert.equal(arr[2].toString(), '_foo') assert.equal(arr[3].toString(), '_foo') assert.deepEqual(s.values, { '0': '', '1': '_foo', '2': 'bar', '3': '' }) arr[1].index = 1.1 assert.deepEqual(s.values, { '0': '', '1': '_foo', '2': 'bar', '3': '' }) s = c('${1:`!p snip.rv = t[2]`} ${2:`!p snip.rv = t[1]`}') assert.deepEqual(s.orderedPyIndexBlocks, []) }) test('Parser, python CodeBlock with related', () => { const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${1:_foo} ${2:bar} $1 $3 ${3:`!p snip.rv = str(t[1]) + str(t[2])`}') let b = s.pyBlocks[0] expect(b).toBeDefined() expect(b.related).toEqual([1, 2]) }) test('Parser, python CodeBlock by sequence', () => { const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${2:\{${3:`!p foo`}\}} ${1:`!p bar`}') let arr = s.pyBlocks expect(arr[0].code).toBe('foo') expect(arr[1].code).toBe('bar') }) test('Parser, hasPython()', () => { const c = text => { return (new SnippetParser(true)).parse(text) } assert.equal(c('${1:`!p foo`}').hasPythonBlock, true) assert.equal(c('`!p foo`').hasPythonBlock, true) assert.equal(c('$1').hasPythonBlock, false) }) test('Parser, insertBefore', () => { const c = text => { return (new SnippetParser(true)).parse(text) } let m = new Placeholder(2) m.insertBefore('\n') let p = new Placeholder(1) m.parent = p m.insertBefore('\n') { let s = c('start ${1:foo}') p = s.children[1] as Placeholder p.insertBefore('\n') let t = s.children[0] as Text assert.equal(t.value, 'start \n') } { let s = c('${1:foo} end') p = s.children[0] as Placeholder p.insertBefore('\n') let t = s.children[0] as Text assert.equal(t.value, '\n') } }) test('Parser, hasCodeBlock()', () => { const c = text => { return (new SnippetParser(true)).parse(text) } assert.equal(c('${1:`!p foo`}').hasCodeBlock, true) assert.equal(c('`!p foo`').hasCodeBlock, true) assert.equal(c('$1').hasCodeBlock, false) let s = (new SnippetParser(false)).parse('`!p foo`', true) assert.strictEqual(s.hasCodeBlock, false) let len = s.fullLen(s.children[1]) assert.strictEqual(len, 0) }) test('Parser, resolved variable', async () => { const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${1:${VISUAL}} $1') assert.ok(s.children[0] instanceof Placeholder) assert.ok(s.children[0].children[0] instanceof Variable) let v = s.children[0].children[0] as Variable assert.equal(v.name, 'VISUAL') { let s = c('`!p`') let m = s.children[0] as CodeBlock await m.resolve(undefined as any) } }) test('Parser, convert and resolve variables', async () => { const c = text => { return (new SnippetParser(false)).parse(text) } { let s = c('${1:${foo}x${foo:bar}} $1') await s.resolveVariables({ resolve: async (variable) => { if (variable.name == 'foo') return 'f' return undefined } }) assert.equal(s.placeholders[0].children.length, 1) assert.equal(s.toString(), 'fxf fxf') } { let s = c('${myname/(.*)$/${1:/capitalize}/}') let variable = s.children[0] as Variable variable.appendChild(new Text('')) expect(s.toTextmateString()).toBe('${myname:/(.*)$/${1:/capitalize}/}') s = s.clone() await s.resolveVariables({ resolve: async (_variable) => { return undefined } }) expect(s.toString()).toBe('Myname') } }) test('Parser, resolved ultisnip variable', async () => { const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${VISUAL/\\w+\\s*/\\u$0\\\\x/} ${visual}') await s.resolveVariables({ resolve: async (variable) => { if (variable.name === 'VISUAL') return 'visual' return '' } }) expect(s.clone().toString()).toBe('Visual\\x ${visual}') }) test('Parser variable with code', () => { // not allowed on ultisnips. const c = text => { return (new SnippetParser(true)).parse(text) } let s = c('${foo:`!p snip.rv = "bar"`}') assert.ok(s.children[0] instanceof Text) assert.ok(s.children[1] instanceof CodeBlock) }) test('Parser, transform condition if text', () => { const p = new SnippetParser(true) let snip = p.parse('begin|${1:t}${1/(t)$|(a)$|(.*)/(?1:abular)(?2:rray)/}') expect(snip.toString()).toBe('begin|tabular') let m = snip.placeholders.find(o => o.index == 1 && o.primary) m.setOnlyChild(new Text('a')) snip.onPlaceholderUpdate(m) expect(snip.toString()).toBe('begin|array') }) test('Parser, transform condition not match', () => { const p = new SnippetParser(true) let snip = p.parse('${1:xyz} ${1/^(f)(b?)/(?2:_:two)/}') expect(snip.toString()).toBe('xyz xyz') }) test('Parser, transform backslash in condition', () => { const p = new SnippetParser(true) let snip = p.parse('${1:foo} ${1/^(f)/(?1:x\\)\\:a:two)/}') expect(snip.toString()).toBe('foo x):aoo') }) test('Parser, transform backslash in format string', () => { const p = new SnippetParser(true) let snip = p.parse('${1:\\n} ${1/^(\\\\n)/$1aa/}') expect(snip.toString()).toBe('\\n \\naa') }) test('Parser, ultisnips transform replacement', () => { const p = new SnippetParser(true) let snip = p.parse('${1:foo} ${1/^\\w/$0_/}') expect(snip.toString()).toBe('foo f_oo') snip = p.parse('${1:foo} ${1/^\\w//}') expect(snip.toString()).toBe('foo oo') snip = p.parse('${1:Foo} ${1/^(\\w+)$/\\u$1 (?1:-\\l$1)/g}') expect(snip.toString()).toBe('Foo Foo -foo') }) test('Parser, convert ultisnips regex', () => { const p = new SnippetParser(true) let snip = p.parse('${1:foo} ${1/^\\A/_/}') expect(snip.toString()).toBe('foo _foo') }) test('Parser, transform condition else text', () => { const p = new SnippetParser(true) let snip = p.parse('${1:foo} ${1/^(f)(b?)/(?2:_:two)/}') expect(snip.toString()).toBe('foo twooo') let m = snip.placeholders.find(o => o.index == 1 && o.primary) m.setOnlyChild(new Text('fb')) snip.onPlaceholderUpdate(m) expect(snip.toString()).toBe('fb _') }) test('Parser, transform escape sequence', () => { const p = new SnippetParser(true) const snip = p.parse('${1:a text}\n${1/\\w+\\s*/\\u$0/}') expect(snip.toString()).toBe('a text\nA text') }) test('Parser, transform backslash', () => { const p = new SnippetParser(true) const snip = p.parse('${1:a}\n${1/\\w+/\\(\\)\\:\\x\\\\y/}') expect(snip.toString()).toBe('a\n():\\x\\y') }) test('Parser, transform with ascii option', () => { let p = new SnippetParser() let snip = p.parse('${1:pêche}\n${1/.*/$0/a}') expect(snip.toString()).toBe('pêche\npeche') p = new SnippetParser() snip = p.parse('${1/.*/$0/a}\n${1:pêche}') expect(snip.toString()).toBe('peche\npêche') }) test('Parser, placeholder with transform', () => { const p = new SnippetParser() const snippet = p.parse('${1:type}${1/(.+)/ /}') let s = snippet.toString() assert.equal(s.length, 5) }) test('Parser, placeholder transforms', function() { assertTextAndMarker('${1///}', '', Placeholder) assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder) assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder) assertTextAndMarker('${1///}', '', Placeholder) // tricky regex assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder) assertMarker('${1/regex\/format/options}', Text) // incomplete assertTextAndMarker('${1///', '${1///', Text) assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text) }) test('No way to escape forward slash in snippet regex #36715', function() { assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable) }) test('No way to escape forward slash in snippet format section #37562', function() { assertMarker('${TM_SELECTED_TEXT/a/\\/$1/g}', Variable) assertMarker('${TM_SELECTED_TEXT/a/in\\/$1ner/g}', Variable) assertMarker('${TM_SELECTED_TEXT/a/end\\//g}', Variable) }) test('Parser, placeholder with choice', () => { assertTextAndMarker('${1|one,two,three|}', 'one', Placeholder) assertTextAndMarker('${1|one|}', 'one', Placeholder) assertTextAndMarker('${1|one1,two2|}', 'one1', Placeholder) assertTextAndMarker('${1|one1\\,two2|}', 'one1,two2', Placeholder) assertTextAndMarker('${1|one1\\|two2|}', 'one1|two2', Placeholder) assertTextAndMarker('${1|one1\\atwo2|}', 'one1\\atwo2', Placeholder) assertTextAndMarker('${1|one,two,three,|}', '${1|one,two,three,|}', Text) assertTextAndMarker('${1|one,', '${1|one,', Text) const p = new SnippetParser() const snippet = p.parse('${1|one,two,three|}') assertMarker(snippet, Placeholder) const expected = [Placeholder, Text, Text, Text] snippet.walk(marker => { assert.equal(marker, expected.shift()) return true }) }) test('Snippet choices: unable to escape comma and pipe, #31521', function() { assertTextAndMarker('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(not, not);', Text, Placeholder, Text) }) test('Marker, basic toTextmateString', function() { function assertTextsnippetString(input: string, expected: string): void { const snippet = new SnippetParser().parse(input) const actual = snippet.toTextmateString() assert.equal(actual, expected) } assertTextsnippetString('$1', '$1') assertTextsnippetString('\\$1', '\\$1') assertTextsnippetString('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(${1|not\\, not, five, 5, 1 23|});') assertTextsnippetString('console.log(${1|not\\, not, \\| five, 5, 1 23|});', 'console.log(${1|not\\, not, \\| five, 5, 1 23|});') assertTextsnippetString('this is text', 'this is text') assertTextsnippetString('this ${1:is ${2:nested with $var}}', 'this ${1:is ${2:nested with ${var}}}') assertTextsnippetString('this ${1:is ${2:nested with $var}}}', 'this ${1:is ${2:nested with ${var}}}\\}') { const snippet = new SnippetParser(true).parse('${1:Foo} ${1/^(\\w+)$/\\x\\u$1/g}') const actual = snippet.children[2].toTextmateString() expect(actual).toBe('${1:\\\\xFoo/^(\\w+)$/\\\\x\\u${1}/g}') } }) test('Marker, toTextmateString() <-> identity', function() { function assertIdent(input: string): void { // full loop: (1) parse input, (2) generate textmate string, (3) parse, (4) ensure both trees are equal const snippet = new SnippetParser().parse(input) const input2 = snippet.toTextmateString() const snippet2 = new SnippetParser().parse(input2) function checkCheckChildren(marker1: Marker, marker2: Marker) { assert.ok(marker1 instanceof Object.getPrototypeOf(marker2).constructor) assert.ok(marker2 instanceof Object.getPrototypeOf(marker1).constructor) assert.equal(marker1.children.length, marker2.children.length) assert.equal(marker1.toString(), marker2.toString()) for (let i = 0; i < marker1.children.length; i++) { checkCheckChildren(marker1.children[i], marker2.children[i]) } } checkCheckChildren(snippet, snippet2) } assertIdent('$1') assertIdent('\\$1') assertIdent('console.log(${1|not\\, not, five, 5, 1 23|});') assertIdent('console.log(${1|not\\, not, \\| five, 5, 1 23|});') assertIdent('this is text') assertIdent('this ${1:is ${2:nested with $var}}') assertIdent('this ${1:is ${2:nested with $var}}}') assertIdent('this ${1:is ${2:nested with $var}} and repeating $1') }) test('Parser, choice marker', () => { const { placeholders } = new SnippetParser().parse('${1|one,two,three|}') assert.equal(placeholders.length, 1) assert.ok(placeholders[0].choice instanceof Choice) assert.ok(placeholders[0].choice.clone() instanceof Choice) assert.ok(placeholders[0].children[0] instanceof Choice) assert.equal((placeholders[0].children[0]).options.length, 3) assertText('${1|one,two,three|}', 'one') assertText('\\${1|one,two,three|}', '${1|one,two,three|}') assertText('${1\\|one,two,three|}', '${1\\|one,two,three|}') assertText('${1||}', '${1||}') }) test('Backslash character escape in choice tabstop doesn\'t work #58494', function() { const { placeholders } = new SnippetParser().parse('${1|\\,,},$,\\|,\\\\|}') assert.equal(placeholders.length, 1) assert.ok(placeholders[0].choice instanceof Choice) }) test('Parser, only textmate', () => { const p = new SnippetParser() assertMarker(p.parse('far{{}}boo'), Text) assertMarker(p.parse('far{{123}}boo'), Text) assertMarker(p.parse('far\\{{123}}boo'), Text) assertMarker(p.parse('far$0boo'), Text, Placeholder, Text) assertMarker(p.parse('far${123}boo'), Text, Placeholder, Text) assertMarker(p.parse('far\\${123}boo'), Text) }) test('Parser, real world', () => { let marker = new SnippetParser().parse('console.warn(${1: $TM_SELECTED_TEXT })').children assert.equal(marker[0].toString(), 'console.warn(') assert.ok(marker[1] instanceof Placeholder) assert.equal(marker[2].toString(), ')') const placeholder = marker[1] assert.equal(placeholder, false) assert.equal(placeholder.index, '1') assert.equal(placeholder.children.length, 3) assert.ok(placeholder.children[0] instanceof Text) assert.ok(placeholder.children[1] instanceof Variable) assert.ok(placeholder.children[1].clone() instanceof Variable) assert.ok(placeholder.children[2] instanceof Text) assert.equal(placeholder.children[0].toString(), ' ') assert.equal(placeholder.children[1].toString(), '') assert.equal(placeholder.children[2].toString(), ' ') const nestedVariable = placeholder.children[1] assert.equal(nestedVariable.name, 'TM_SELECTED_TEXT') assert.equal(nestedVariable.children.length, 0) marker = new SnippetParser().parse('$TM_SELECTED_TEXT').children assert.equal(marker.length, 1) assert.ok(marker[0] instanceof Variable) }) test('Parser, transform example', () => { let { children } = new SnippetParser().parse('${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0') //${1:name} assert.ok(children[0] instanceof Placeholder) assert.equal(children[0].children.length, 1) assert.equal(children[0].children[0].toString(), 'name') assert.equal((children[0]).transform, undefined) // : assert.ok(children[1] instanceof Text) assert.equal(children[1].toString(), ' : ') //${2:type} assert.ok(children[2] instanceof Placeholder) assert.equal(children[2].children.length, 1) assert.equal(children[2].children[0].toString(), 'type') //${3/\\s:=(.*)/${1:+ :=}${1}/} assert.ok(children[3] instanceof Placeholder) assert.equal(children[3].children.length, 1) assert.notEqual((children[3]).transform, undefined) let transform = (children[3]).transform assert.equal(transform.regexp, '/\\s:=(.*)/') assert.equal(transform.children.length, 2) assert.ok(transform.children[0] instanceof FormatString) assert.equal((transform.children[0]).index, 1) assert.equal((transform.children[0]).ifValue, ' :=') assert.ok(transform.children[1] instanceof FormatString) assert.equal((transform.children[1]).index, 1) assert.ok(children[4] instanceof Text) assert.equal(children[4].toString(), ';\n') }) test('Parser, ConditionString', () => { assert.ok(ConditionString != undefined) let s = new ConditionString(0, 'if', 'else') assert.strictEqual(s.toTextmateString(), '(?0:if:else)') s = new ConditionString(0, 'if', '') assert.strictEqual(s.clone().toTextmateString(), '(?0:if)') // invalid examples assertText('$1 ${1/.*/(?p:foo:bar)/}', ' (?p:foo:bar)', true) assertText('$1 ${1/.*/(?1foobar)/}', ' (?1foobar)', true) assertText('$1 ${1/.*/(?1:foo:bar/}', ' (?1:foo:bar', true) assertText('$1 ${1/.*/', ' ${1/.*/', true) assertText('${foo', '${foo') assertText('$foo', '$foo', true) assertText('${foo}', '${foo}', true) assertText('$1 ${1/.*/(?1:', ' ${1/.*/(?1:', true) }) test('Parser, FormatString', () => { let { children } = new SnippetParser().parse('${foo/^x/complex${1:?if:else}/i}') let transform = children[0]['transform'] as Transform let res = transform.resolve('y') assert.strictEqual(res, 'complexelse') let formatString = transform.children[1] as FormatString assert.strictEqual(formatString.toTextmateString(), '${1:?if:else}') let assertResolve = (shorthandName: string, value: string, result: string) => { let formatString = new FormatString(0, shorthandName) assert.strictEqual(formatString.resolve(value), result) assert.ok(formatString.toTextmateString().includes(shorthandName)) } assertResolve('upcase', '', '') assertResolve('downcase', '', '') assertResolve('capitalize', '', '') assertResolve('pascalcase', '', '') assertResolve('pascalcase', '', '') assertResolve('pascalcase', '1', '1') let f = new FormatString(0, undefined, 'if', undefined) assert.strictEqual(f.toTextmateString(), '${0:+if}') f = new FormatString(0, undefined, undefined, 'else') assert.strictEqual(f.toTextmateString(), '${0:-else}') }) test('Parser, default placeholder values', () => { assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder) const [, p1, , p2] = new SnippetParser().parse('errorContext: `${1:err}`, error:$1').children assert.equal((p1).index, '1') assert.equal((p1).children.length, '1') assert.equal(((p1).children[0]), 'err') assert.equal((p2).index, '1') assert.equal((p2).children.length, '1') assert.equal(((p2).children[0]), 'err') }) test('Parser, default placeholder values and one transform', () => { assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder) const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children assert.equal((p3).index, '1') assert.equal((p3).children.length, '1') assert.equal(((p3).children[0]), 'err') assert.equal((p3).transform, undefined) assert.equal((p4).index, '1') assert.equal((p4).children.length, '1') assert.equal(((p4).children[0]), 'ok') assert.notEqual((p4).transform, undefined) }) test('Repeated snippet placeholder should always inherit, #31040', function() { assertText('${1:foo}-abc-$1', 'foo-abc-foo') assertText('${1:foo}-abc-${1}', 'foo-abc-foo') assertText('${1:foo}-abc-${1:bar}', 'foo-abc-foo') assertText('${1}-abc-${1:foo}', 'foo-abc-foo') }) test('backspace esapce in TM only, #16212', () => { const actual = new SnippetParser().text('Foo \\\\${abc}bar') assert.equal(actual, 'Foo \\bar') }) test('colon as variable/placeholder value, #16717', () => { let actual = new SnippetParser().text('${TM_SELECTED_TEXT:foo:bar}') assert.equal(actual, 'foo:bar') actual = new SnippetParser().text('${1:foo:bar}') assert.equal(actual, 'foo:bar') }) test('incomplete placeholder', () => { assertTextAndMarker('${1:}', '', Placeholder) }) test('marker#len', () => { function assertLen(template: string, ...lengths: number[]): void { const snippet = new SnippetParser().parse(template, true) snippet.walk(m => { const expected = lengths.shift() assert.equal(m.len(), expected) return true }) assert.equal(lengths.length, 0) } assertLen('text$0', 4, 0) assertLen('$1text$0', 0, 4, 0) assertLen('te$1xt$0', 2, 0, 2, 0) assertLen('errorContext: `${1:err}`, error: $0', 15, 0, 3, 10, 0) assertLen('errorContext: `${1:err}`, error: $1$0', 15, 0, 3, 10, 0, 3, 0) assertLen('$TM_SELECTED_TEXT$0', 0, 0) assertLen('${TM_SELECTED_TEXT:def}$0', 0, 3, 0) }) test('marker#replaceWith', () => { let m = new Placeholder(1) expect(m.replaceWith(new Text(''))).toBe(false) let p = new Placeholder(2) p.appendChild(m) p.replaceChildren([]) expect(m.replaceWith(new Text(''))).toBe(false) }) test('parser, parent node', function() { let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true) assert.equal(snippet.placeholders.length, 3) let [first, second] = snippet.placeholders assert.equal(first.index, '1') assert.equal(second.index, '2') assert.ok(second.parent === first) assert.ok(first.parent === snippet) snippet = new SnippetParser().parse('${VAR:default${1:value}}$0', true) assert.equal(snippet.placeholders.length, 2) ;[first] = snippet.placeholders assert.equal(first.index, '1') assert.ok(snippet.children[0] instanceof Variable) assert.ok(first.parent === snippet.children[0]) }) test('Maximum call stack size exceeded, #28983', () => { new SnippetParser().parse('${1:${foo:${1}}}') }) test('Snippet can freeze the editor, #30407', () => { const seen = new Set() seen.clear() new SnippetParser().parse('class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend').walk(marker => { assert.ok(!seen.has(marker)) seen.add(marker) return true }) seen.clear() new SnippetParser().parse('${1:${FOO:abc$1def}}').walk(marker => { assert.ok(!seen.has(marker)) seen.add(marker) return true }) }) test('Snippets: make parser ignore `${0|choice|}`, #31599', function() { assertTextAndMarker('${0|foo,bar|}', '${0|foo,bar|}', Text) assertTextAndMarker('${1|foo,bar|}', 'foo', Placeholder) }) test('Transform -> FormatString#resolve', function() { // shorthand functions assert.equal(new FormatString(1, 'upcase').resolve('foo'), 'FOO') assert.equal(new FormatString(1, 'downcase').resolve('FOO'), 'foo') assert.equal(new FormatString(1, 'capitalize').resolve('bar'), 'Bar') assert.equal(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat') assert.equal(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo') assert.equal(new FormatString(1, 'notKnown').resolve('input'), 'input') // if assert.equal(new FormatString(1, undefined, 'foo', undefined).resolve(undefined), '') assert.equal(new FormatString(1, undefined, 'foo', undefined).resolve(''), '') assert.equal(new FormatString(1, undefined, 'foo', undefined).resolve('bar'), 'foo') // else assert.equal(new FormatString(1, undefined, undefined, 'foo').resolve(undefined), 'foo') assert.equal(new FormatString(1, undefined, undefined, 'foo').resolve(''), 'foo') assert.equal(new FormatString(1, undefined, undefined, 'foo').resolve('bar'), 'bar') // if-else assert.equal(new FormatString(1, undefined, 'bar', 'foo').resolve(undefined), 'foo') assert.equal(new FormatString(1, undefined, 'bar', 'foo').resolve(''), 'foo') assert.equal(new FormatString(1, undefined, 'bar', 'foo').resolve('baz'), 'bar') }) test('Snippet variable transformation doesn\'t work if regex is complicated and snippet body contains \'$$\' #55627', function() { const snippet = new SnippetParser().parse('const fileName = "${TM_FILENAME/(.*)\\..+$/$1/}"') assert.equal(snippet.toTextmateString(), 'const fileName = "${TM_FILENAME/(.*)\\..+$/${1}/}"') }) test('[BUG] HTML attribute suggestions: Snippet session does not have end-position set, #33147', function() { const { placeholders } = new SnippetParser().parse('src="$1"', true) const [first, second] = placeholders assert.equal(placeholders.length, 2) assert.equal(first.index, 1) assert.equal(second.index, 0) }) test('Snippet optional transforms are not applied correctly when reusing the same variable, #37702', function() { const transform = new Transform() assert.strictEqual(transform.toString(), '') transform.appendChild(new FormatString(1, 'upcase')) transform.appendChild(new FormatString(2, 'upcase')) transform.regexp = /^(.)|-(.)/g assert.equal(transform.resolve('my-file-name'), 'MyFileName') const clone = transform.clone() assert.equal(clone.resolve('my-file-name'), 'MyFileName') transform.regexp = /^(.)|-(.)/i assert.strictEqual(transform.clone().regexp.ignoreCase, true) }) test('problem with snippets regex #40570', function() { const snippet = new SnippetParser().parse('${TM_DIRECTORY/.*src[\\/](.*)/$1/}') assertMarker(snippet, Variable) }) test('Variable transformation doesn\'t work if undefined variables are used in the same snippet #51769', function() { let transform = new Transform() transform.appendChild(new Text('bar')) transform.regexp = new RegExp('foo', 'gi') assert.equal(transform.toTextmateString(), '/foo/bar/ig') }) test('Snippet parser freeze #53144', function() { let snippet = new SnippetParser().parse('${1/(void$)|(.+)/${1:?-\treturn nil;}/}') assertMarker(snippet, Placeholder) }) test('snippets variable not resolved in JSON proposal #52931', function() { assertTextAndMarker('FOO${1:/bin/bash}', 'FOO/bin/bash', Text, Placeholder) }) test('Mirroring sequence of nested placeholders not selected properly on backjumping #58736', function() { let snippet = new SnippetParser().parse('${3:nest1 ${1:nest2 ${2:nest3}}} $3') assert.equal(snippet.children.length, 3) assert.ok(snippet.children[0] instanceof Placeholder) assert.ok(snippet.children[1] instanceof Text) assert.ok(snippet.children[2] instanceof Placeholder) function assertParent(marker: Marker) { marker.children.forEach(assertParent) if (!(marker instanceof Placeholder)) { return } let found = false let m: Marker = marker while (m && !found) { if (m.parent === snippet) { found = true } m = m.parent } assert.ok(found) } let [, , clone] = snippet.children assertParent(clone) }) }) describe('TextmateSnippet', () => { test('TextmateSnippet#enclosingPlaceholders', () => { let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true) let [first, second] = snippet.placeholders assert.deepEqual(snippet.enclosingPlaceholders(first), []) assert.deepEqual(snippet.enclosingPlaceholders(second), [first]) }) test('TextmateSnippet#getTextBefore', () => { let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true) expect(snippet.getTextBefore(snippet, undefined)).toBe('') let [first, second] = snippet.placeholders expect(snippet.getTextBefore(second, first)).toBe('is ') snippet = new SnippetParser().parse('This ${1:foo ${2:is ${3:nested}}} $0', true) let arr = snippet.placeholders expect(snippet.getTextBefore(arr[2], arr[0])).toBe('foo is ') }) test('TextmateSnippet#offset', () => { let snippet = new SnippetParser().parse('te$1xt', true) assert.equal(snippet.offset(snippet.children[0]), 0) assert.equal(snippet.offset(snippet.children[1]), 2) assert.equal(snippet.offset(snippet.children[2]), 2) snippet = new SnippetParser().parse('${TM_SELECTED_TEXT:def}', true) assert.equal(snippet.offset(snippet.children[0]), 0) assert.equal(snippet.offset((snippet.children[0]).children[0]), 0) // foreign marker assert.equal(snippet.offset(new Text('foo')), -1) }) test('TextmateSnippet#placeholder', () => { let snippet = new SnippetParser().parse('te$1xt$0', true) let placeholders = snippet.placeholders assert.equal(placeholders.length, 2) snippet = new SnippetParser().parse('te$1xt$1$0', true) placeholders = snippet.placeholders assert.equal(placeholders.length, 3) snippet = new SnippetParser().parse('te$1xt$2$0', true) placeholders = snippet.placeholders assert.equal(placeholders.length, 3) snippet = new SnippetParser().parse('${1:bar${2:foo}bar}$0', true) placeholders = snippet.placeholders assert.equal(placeholders.length, 3) }) test('TextmateSnippet#replace 1/2', function() { let snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true) assert.equal(snippet.placeholders.length, 3) const [, second] = snippet.placeholders assert.equal(second.index, '2') const enclosing = snippet.enclosingPlaceholders(second) assert.equal(enclosing.length, 1) assert.equal(enclosing[0].index, '1') let marker = snippet.placeholders.find(o => o.index == 2) let nested = new SnippetParser().parse('ddd$1eee', false) let err try { snippet.replace(marker, nested.children) } catch (e) { err = e } expect(err).toBeDefined() }) test('TextmateSnippet#replace 2/2', () => { let snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true) assert.equal(snippet.placeholders.length, 3) const [, second] = snippet.placeholders assert.equal(second.index, '2') let nested = new SnippetParser().parse('dddeee$0', true) snippet.replace(second, nested.children) assert.equal(snippet.toString(), 'aaabbbdddeee') assert.equal(snippet.placeholders.length, 4) }) test('TextmateSnippet replace variable with placeholder', async () => { let snippet = new SnippetParser().parse('|${1:${foo}} ${foo} $1 ${bar}|', true) await snippet.resolveVariables({ resolve: _variable => { return undefined } }) let placeholders = snippet.placeholders let indexes = placeholders.map(o => o.index) expect(indexes).toEqual([1, 2, 2, 1, 3, 0]) let p = placeholders.find(o => o.index == 2 && o.primary) p.setOnlyChild(new Text('x')) snippet.onPlaceholderUpdate(p) expect(snippet.toString()).toBe('|x x x bar|') }) test('mergeTexts()', () => { let m = new TextmateSnippet(false) m.replaceChildren([ new Text('c'), new Placeholder(1), new Text('a'), new Text('b'), new Placeholder(2), new Text('c'), new Text(''), new Text('d'), new Text('e') ]) mergeTexts(m, 0) expect(m.hasPythonBlock).toBe(false) expect(m.hasCodeBlock).toBe(false) expect(m.children.length).toBe(5) expect(m.children[2].toString()).toBe('ab') expect(m.children[4].toString()).toBe('cde') }) test('getPlaceholderId', () => { const p = new Placeholder(1) let id = getPlaceholderId(p) expect(typeof id).toBe('number') expect(p.id).toBe(id) expect(getPlaceholderId(p)).toBe(id) }) }) ================================================ FILE: src/__tests__/snippets/session.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import path from 'path' import { Position, Range, TextEdit } from 'vscode-languageserver-protocol' import { SnippetConfig, SnippetEdit, SnippetSession } from '../../snippets/session' import { UltiSnippetContext } from '../../snippets/util' import { Disposable, disposeAll } from '../../util' import window from '../../window' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim let disposables: Disposable[] = [] beforeAll(async () => { await helper.setup() nvim = helper.nvim let pyfile = path.join(__dirname, '../ultisnips.py') await nvim.command(`execute 'pyxfile '.fnameescape('${pyfile}')`) }) afterAll(async () => { await helper.shutdown() }) afterEach(async () => { disposeAll(disposables) await helper.reset() }) async function createSession(enableHighlight = false, preferComplete = false, nextOnDelete = false): Promise { let doc = await workspace.document let config: SnippetConfig = { highlight: enableHighlight, preferComplete, nextOnDelete } let session = new SnippetSession(nvim, doc, config) disposables.push(session) disposables.push(workspace.onDidChangeTextDocument(e => { if (e.bufnr == session.bufnr) session.onChange(e) })) return session } describe('SnippetSession', () => { const defaultRange = Range.create(0, 0, 0, 0) const defaultContext = { id: `1-1`, line: '', range: defaultRange } async function start(inserted: string, range = defaultRange, select = true, context?: UltiSnippetContext): Promise { await nvim.input('i') let doc = await workspace.document let session = new SnippetSession(nvim, doc, { highlight: false, nextOnDelete: false, preferComplete: false }) return await session.start(inserted, range, select, context) } async function getCursorRange(): Promise { let pos = await window.getCursorPosition() return Range.create(pos, pos) } describe('start()', () => { it('should not activate when insert empty snippet', async () => { let res = await start('', defaultRange) expect(res).toBe(false) }) it('should insert escaped text', async () => { let res = await start('\\`a\\` \\$ \\{\\}', Range.create(0, 0, 0, 0), false, defaultContext) expect(res).toBe(true) let line = await nvim.line expect(line).toBe('`a` $ {}') }) it('should not start with plain snippet when jump to final placeholder', async () => { let res = await start('bar$0', defaultRange) expect(res).toBe(false) let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 3 }) }) it('should start with range replaced', async () => { await nvim.setLine('foo') let res = await start('bar$0', Range.create(0, 0, 0, 3), true) expect(res).toBe(false) let line = await nvim.line expect(line).toBe('bar') }) it('should fix indent of next line when necessary', async () => { let buf = await nvim.buffer await nvim.setLine(' ab') await nvim.input('i') let session = await createSession() await session.selectCurrentPlaceholder() let res = await session.start('${1:x}\n', Range.create(0, 3, 0, 3)) expect(res).toBe(true) let lines = await buf.lines expect(lines).toEqual([' ax', ' b']) }) it('should insert indent for snippet endsWith line break', async () => { let buf = await nvim.buffer await nvim.setLine(' bar') await nvim.command('startinsert') await nvim.call('cursor', [1, 3]) let session = await createSession() let res = await session.start('${1:foo}\n', Range.create(0, 2, 0, 2)) expect(res).toBe(true) let lines = await buf.lines expect(lines).toEqual([' foo', ' bar']) }) it('should start without select placeholder', async () => { let session = await createSession() let res = await session.start(' ${1:aa} ', defaultRange, false) expect(res).toBe(true) let { mode } = await nvim.mode expect(mode).toBe('n') await session.selectCurrentPlaceholder() await helper.waitFor('mode', [], 's') }) it('should use default variable value', async () => { let session = await createSession() let res = await session.start('${foo:bar}', defaultRange, false) expect(res).toBe(true) let line = await nvim.getLine() expect(line).toBe('bar') }) it('should select none transform placeholder', async () => { await start('${1/..*/ -> /}xy$1', defaultRange) let col = await nvim.call('col', '.') expect(col).toBe(3) }) it('should indent multiple lines variable text', async () => { let buf = await nvim.buffer let text = 'abc\n def' await nvim.setVar('coc_selected_text', text) await start('fun\n ${0:${TM_SELECTED_TEXT:return}}\nend') let lines = await buf.lines expect(lines.length).toBe(4) expect(lines).toEqual([ 'fun', ' abc', ' def', 'end' ]) let val = await nvim.getVar('coc_selected_text') expect(val).toBe(null) }) it('should resolve VISUAL', async () => { let text = 'abc' await nvim.setVar('coc_selected_text', text) await start('$VISUAL') let line = await nvim.line expect(line).toBe('abc') }) it('should resolve default value of VISUAL', async () => { await nvim.setVar('coc_selected_text', '') await start('${VISUAL:foo}') let line = await nvim.line expect(line).toBe('foo') }) }) describe('insertSnippetEdits', () => { it('should insert snippets', async () => { await helper.createDocument() let session = await createSession() await helper.createDocument() let doc = session.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\n\nbar')]) let res = await session.insertSnippetEdits([]) expect(res).toBe(false) let edits: SnippetEdit[] = [] edits.push({ range: Range.create(0, 0, 0, 3), snippet: 'foo($1)' }) edits.push({ range: Range.create(2, 0, 2, 3), snippet: 'bar($1)' }) res = await session.insertSnippetEdits(edits) expect(res).toBe(true) let lines = await doc.buffer.lines expect(lines).toEqual(['foo()', '', 'bar()']) let range = session.placeholder!.range expect(range).toEqual(Range.create(0, 4, 0, 4)) let ses = await createSession() res = await ses.insertSnippetEdits([{ range: Range.create(0, 0, 0, 0), snippet: 'foo' }]) expect(res).toBe(true) doc = ses.document let line = doc.getline(0) expect(line).toBe('foo') expect(ses.selected).toBe(false) }) }) describe('nested snippet', () => { it('should start with nest snippet', async () => { let session = await createSession() let res = await session.start('${1:a} ${2:b}', defaultRange, false) let line = await nvim.getLine() expect(line).toBe('a b') expect(res).toBe(true) let { placeholder } = session expect(placeholder.index).toBe(1) res = await session.start('${1:foo} | ${2:bar}', defaultRange) expect(res).toBe(true) placeholder = session.placeholder expect(placeholder.value).toBe('foo') expect(placeholder.index).toBe(1) line = await nvim.getLine() expect(line).toBe('foo | bara b') expect(session.snippet.text).toBe('foo | bara b') await session.nextPlaceholder() placeholder = session.placeholder expect(placeholder.index).toBe(2) expect(session.placeholder.value).toBe('bar') let col = await nvim.call('col', ['.']) expect(col).toBe(9) await session.nextPlaceholder() expect(session.isActive).toBe(true) // should finalize snippet expect(session.placeholder.index).toBe(1) await session.nextPlaceholder() expect(session.placeholder.index).toBe(2) expect(session.placeholder.value).toBe('b') }) it('should start nest snippet without select', async () => { await nvim.command('startinsert') let session = await createSession() let res = await session.start('${1:a} $1', defaultRange) res = await session.start('${1:foo}', Range.create(0, 0, 0, 1), false) expect(res).toBe(true) let line = await nvim.line expect(line).toBe('foo foo') await session.selectCurrentPlaceholder() await session.nextPlaceholder() expect(session.placeholder).toBeDefined() }) it('should not nested when range not contains', async () => { await nvim.command('startinsert') let session = await createSession() let res = await session.start('${1:a} ${2:b}', defaultRange) res = await session.start('${1:foo} ${2:bar}', Range.create(0, 0, 0, 3), false) expect(res).toBe(true) let line = await nvim.line expect(line).toBe('foo bar') }) }) describe('getRanges()', () => { it('should getRanges of placeholder', async () => { async function checkRanges(snippet: string, results: any) { let session = await createSession() await session.start(snippet, defaultRange) let curr = session.placeholder let res = session.snippet.getRanges(curr.marker) expect(res).toEqual(results) session.deactivate() await nvim.setLine('') } await checkRanges('$1 $1', []) await checkRanges('${foo}', [Range.create(0, 0, 0, 3)]) await checkRanges('${2:${1:foo}}', [Range.create(0, 0, 0, 3)]) await checkRanges('${2:${1:foo}} ${2/^_(.*)/$1/}', [Range.create(0, 0, 0, 3)]) }) }) describe('synchronize()', () => { it('should cancel when before and body changed', async () => { let session = await createSession() await nvim.setLine('x') await nvim.input('a') await session.start('${1:foo }bar', defaultRange) await nvim.setLine('yfoo bar') await session.forceSynchronize() expect(session.isActive).toBe(false) }) it('should synchronize content change', async () => { let session = await createSession(true) await session.checkPosition() expect(session.version).toBe(-1) await session.start('${1:foo}${2:`!p snip.rv = ""`} `!p snip.rv = t[1] + t[2]`', defaultRange, true, { id: '1-1', line: '', range: defaultRange }) await nvim.input('bar') await session.forceSynchronize() await helper.waitFor('getline', ['.'], 'bar bar') }) it('should cancel with unexpected change', async () => { let session = await createSession(true) await nvim.setLine('c') await nvim.input('A') await session.start('${1:foo}', Range.create(0, 1, 0, 1)) await nvim.setLine('bxoo') await session.forceSynchronize() expect(session.isActive).toBe(false) }) it('should cancel when document have changed', async () => { let session = await createSession() let doc = await workspace.document await nvim.input('i') await session.start('${2:foo} ${1}', defaultRange) await nvim.setLine('bfoo ') await doc.patchChange() await nvim.setLine('xfoo ') await nvim.call('cursor', [1, 1]) await session.forceSynchronize() expect(session.snippet.text).toBe('xfoo ') expect(session.isActive).toBe(true) }) it('should reset snippet when cancelled', async () => { let session = await createSession() await nvim.input('i') await session.start('${1} `!p snip.rv = t[1]`', defaultRange, false, defaultContext) await nvim.setLine('b ') let cancelled = false let spy = jest.spyOn(session.snippet['_tmSnippet'], 'updatePythonCodes').mockImplementation(() => { return new Promise(resolve => { session.cancel() setImmediate(() => { resolve() cancelled = true }) }) }) await helper.waitValue(() => cancelled, true) expect(session.snippet.text).toBe(' ') spy.mockRestore() await session.onCompleteDone() }) it('should not cancel when change after snippet', async () => { let session = await createSession() await nvim.setLine(' x') await nvim.input('i') await session.start('${1:foo }bar', defaultRange) await nvim.setLine('foo bar y') await session.forceSynchronize() expect(session.isActive).toBe(true) }) it('should cancel when change before and in snippet', async () => { let session = await createSession() await nvim.setLine(' x') await nvim.input('i') await session.start('${1:foo }bar', defaultRange) await nvim.setLine('afoobar') await session.forceSynchronize() expect(session.isActive).toBe(false) }) it('should not cancel when change text', async () => { let session = await createSession() await nvim.input('i') await session.start('${1:foo} bar', defaultRange) await nvim.setLine('foodbar') await session.forceSynchronize() expect(session.isActive).toBe(true) expect(session.snippet.text).toBe('foodbar') }) it('should able to jump when current placeholder destroyed', async () => { let session = await createSession() await nvim.input('i') await session.start('${1:foo} bar', defaultRange) await nvim.setLine('fobar') await session.forceSynchronize() expect(session.isActive).toBe(true) await session.nextPlaceholder() expect(session.isActive).toBe(false) }) it('should adjust with removed text', async () => { let session = await createSession() await nvim.input('i') await session.start('${1:foo} bar$0', defaultRange) await nvim.input('') await nvim.call('cursor', [1, 5]) await nvim.input('i') await nvim.input('') await helper.wait(1) await session.forceSynchronize() expect(session.isActive).toBe(true) await session.nextPlaceholder() let col = await nvim.call('col', ['.']) expect(col).toBe(7) }) it('should automatically select next placeholder', async () => { let session = await createSession(false, false, true) await nvim.input('i') await session.start('${1:foo} bar$0', defaultRange) await nvim.input('') await session.forceSynchronize() let placeholder = session.placeholder expect(placeholder.index).toBe(0) }) it('should changed none current placeholder', async () => { let session = await createSession() await nvim.input('i') await session.start('$1 $2', defaultRange) await nvim.input('A') await nvim.input(' ') await session.forceSynchronize() expect(session.isActive).toBe(true) let placeholder = session.snippet.getPlaceholderByIndex(2) expect(placeholder.value).toBe(' ') let p = session.placeholder expect(p.index).toBe(1) }) it('should update cursor column after synchronize', async () => { let session = await createSession() await nvim.input('i') await session.start('${1} ${1:foo}', defaultRange) await nvim.input('b') await session.forceSynchronize() let pos = await window.getCursorPosition() expect(pos).toEqual(Position.create(0, 3)) await nvim.input('a') await session.forceSynchronize() pos = await window.getCursorPosition() let line = await nvim.line expect(line).toEqual('ba ba') expect(pos).toEqual(Position.create(0, 5)) await nvim.input('') await session.forceSynchronize() pos = await window.getCursorPosition() expect(pos).toEqual(Position.create(0, 3)) line = await nvim.line expect(line).toBe('b b') }) it('should update cursor line after synchronize', async () => { let buf = await nvim.buffer let session = await createSession() await nvim.input('i') await session.start('${1} ${1:foo}x', defaultRange) await nvim.input('b') await session.forceSynchronize() let pos = await window.getCursorPosition() expect(pos).toEqual(Position.create(0, 3)) await nvim.input('') await session.forceSynchronize() expect(session.isActive).toBe(true) let lines = await buf.lines expect(lines).toEqual(['b', ' b', 'x']) pos = await window.getCursorPosition() expect(pos).toEqual(Position.create(2, 0)) }) it('should synchronize changes at the same time', async () => { await nvim.input('i') let doc = await workspace.document let session = await createSession() let res = await session.start('|$1 $1|', defaultRange) expect(res).toBe(true) let line = await nvim.line expect(line).toBe('| |') let p = new Promise(resolve => { doc.onDocumentChange(_e => { resolve(undefined) }) }) await nvim.input('xy') await p await doc.applyEdits([TextEdit.replace(Range.create(0, 1, 0, 3), '')]) await session.forceSynchronize() line = await nvim.line expect(line).toBe('| |') }) it('should deactivate when synchronize text is wrong', async () => { let doc = await workspace.document let session = await createSession() let res = await session.start('${1:foo}', defaultRange) expect(res).toBe(true) let spy = jest.spyOn(session.snippet, 'replaceWithText').mockImplementation(() => { return Promise.resolve({ snippetText: 'xy', marker: undefined }) }) await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'p')]) await session.forceSynchronize() spy.mockRestore() expect(session.isActive).toBe(false) }) it('should reset position when change before snippet', async () => { let session = await createSession() await nvim.setLine('x') await nvim.input('a') let r = await getCursorRange() await session.start('${1:foo} bar', r) await nvim.call('coc#cursor#move_to', [0, 0]) await nvim.command('startinsert') await nvim.setLine('yfoo bar') await session.forceSynchronize() expect(session.isActive).toBe(true) let start = session.snippet.start expect(start).toEqual(Position.create(0, 1)) session.deactivate() }) it('should cancel change synchronize', async () => { let doc = await workspace.document let session = await createSession() let res = await session.start('${1:foo}', defaultRange) expect(res).toBe(true) session.cancel(true) await doc.applyEdits([TextEdit.insert(Position.create(0, 1), 'x')]) process.nextTick(() => { session.cancel() }) await session._synchronize() expect(session.snippet.tmSnippet.toString()).toBe('foo') }) }) describe('deactivate()', () => { it('should deactivate on cursor outside', async () => { let buf = await nvim.buffer let session = await createSession() let res = await session.start('a${1:a}b', defaultRange) expect(res).toBe(true) await buf.append(['foo', 'bar']) await nvim.call('cursor', [2, 2]) await session.checkPosition() expect(session.isActive).toBe(false) }) it('should not throw when jump on deactivate session', async () => { let session = await createSession() session.deactivate() await session.start('${1:foo} $0', defaultRange) await session.selectPlaceholder(undefined) await session.forceSynchronize() await session.previousPlaceholder() await session.nextPlaceholder() }) it('should cancel keymap on jump final placeholder', async () => { let session = await createSession() await nvim.input('i') await session.start('$0x${1:a}b$0', defaultRange) let line = await nvim.line expect(line).toBe('xab') let map = await nvim.call('maparg', ['', 'i']) as string expect(map).toMatch('coc#snippet#jump') await session.nextPlaceholder() map = await nvim.call('maparg', ['', 'i']) as string expect(map).toBe('') }) }) describe('nextPlaceholder()', () => { it('should not throw when session not activated', async () => { let session = await createSession() await session.start('${foo} ${bar}', defaultRange, false) session.deactivate() await session.nextPlaceholder() await session.previousPlaceholder() }) it('should jump to variable placeholder', async () => { let session = await createSession() await session.start('${foo} ${bar}', defaultRange, false) await session.selectCurrentPlaceholder() await session.nextPlaceholder() let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 6 }) }) it('should jump to variable placeholder after number placeholder', async () => { let session = await createSession() await session.start('${foo} ${1:bar}', defaultRange, false) await session.selectCurrentPlaceholder() await session.nextPlaceholder() let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 2 }) }) it('should jump to first placeholder', async () => { let session = await createSession() await session.start('${foo} ${foo} ${2:bar}', defaultRange, false) await session.selectCurrentPlaceholder() let pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 10 }) await session.nextPlaceholder() pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 2 }) await session.nextPlaceholder() pos = await window.getCursorPosition() expect(pos).toEqual({ line: 0, character: 11 }) }) it('should goto next placeholder', async () => { let session = await createSession() let res = await session.start('${1:a} ${2:b} c', defaultRange) expect(res).toBe(true) await session.nextPlaceholder() let { placeholder } = session expect(placeholder.index).toBe(2) }) it('should jump to none transform placeholder', async () => { let session = await createSession() let res = await session.start('${1} ${2/^_(.*)/$2/}bar$2', defaultRange) expect(res).toBe(true) let line = await nvim.line expect(line).toBe(' bar') await session.nextPlaceholder() let col = await nvim.call('col', '.') expect(col).toBe(5) }) it('should remove white space on jump', async () => { let session = await createSession() let opts = { removeWhiteSpace: true, ...defaultContext } let res = await session.start('foo $1\n${2:bar} $0', defaultRange, true, opts) expect(res).toBe(true) let line = await nvim.line expect(line).toBe('foo ') await session.nextPlaceholder() expect(session.isActive).toBe(true) let lines = await session.document.buffer.lines expect(lines[0]).toBe('foo') let p = session.placeholder await session.removeWhiteSpaceBefore(p) }) }) describe('previousPlaceholder()', () => { it('should goto previous placeholder', async () => { let session = await createSession() let res = await session.start('${1:foo} ${2:bar}', defaultRange) expect(res).toBe(true) await session.nextPlaceholder() expect(session.placeholder.index).toBe(2) await session.previousPlaceholder() expect(session.placeholder.index).toBe(1) }) }) describe('highlights()', () => { it('should add highlights', async () => { let ns = await nvim.call('coc#highlight#create_namespace', ['snippets']) as number let session = await createSession(true) await session.start('${2:bar ${1:foo}} $2', defaultRange) await session.nextPlaceholder() let buf = nvim.createBuffer(workspace.bufnr) let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) expect(markers.length).toBe(2) expect(markers[0][3].hl_group).toBe('CocSnippetVisual') expect(markers[1][3].hl_group).toBe('CocSnippetVisual') session.deactivate() }) }) describe('checkPosition()', () => { it('should cancel snippet if position out of range', async () => { let session = await createSession() await nvim.setLine('bar') await session.start('${1:foo}', defaultRange) await nvim.call('cursor', [1, 5]) await session.checkPosition() expect(session.isActive).toBe(false) }) it('should not cancel snippet if position in range', async () => { let session = await createSession() await session.start('${1:foo}', defaultRange) await nvim.call('cursor', [1, 3]) await session.checkPosition() expect(session.isActive).toBe(true) }) }) describe('resolveSnippet()', () => { it('should resolveSnippet', async () => { let session = await createSession() let res = await session.resolveSnippet(nvim, '${1:`!p snip.rv = "foo"`}', { line: 'foo', range: Range.create(0, 0, 0, 3) }) expect(res).toBe('foo') }) }) describe('selectPlaceholder()', () => { it('should select range placeholder', async () => { let session = await createSession() await session.start('${1:abc}', defaultRange) let mode = await nvim.mode expect(mode.mode).toBe('s') await nvim.input('') let line = await nvim.line expect(line).toBe('') }) it('should select empty placeholder', async () => { let session = await createSession() await session.start('a ${1} ${2}', defaultRange) let mode = await nvim.mode expect(mode.mode).toBe('i') let col = await nvim.call('col', '.') expect(col).toBe(3) }) it('should select choice placeholder', async () => { await nvim.input('i') let session = await createSession() await session.start('${1|one,two,three|}', defaultRange) let line = await nvim.line expect(line).toBe('one') await helper.waitPopup() let items = await helper.items() expect(items.length).toBe(3) }) }) }) ================================================ FILE: src/__tests__/snippets/snippet.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import * as assert from 'assert' import path from 'path' import { CancellationToken, CancellationTokenSource } from 'vscode-languageserver-protocol' import { Position, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import events from '../../events' import { addPythonTryCatch, executePythonCode, generateContextId, getInitialPythonCode, getVariablesCode, hasPython } from '../../snippets/eval' import { CodeBlock, Placeholder, SnippetParser, Text, TextmateSnippet } from '../../snippets/parser' import { CocSnippet, getNextPlaceholder, getUltiSnipActionCodes } from '../../snippets/snippet' import { SnippetString } from '../../snippets/string' import { convertRegex, getTextAfter, getTextBefore, normalizeSnippetString, shouldFormat, toSnippetString, UltiSnippetContext } from '../../snippets/util' import { padZero, parseComments, parseCommentstring, SnippetVariableResolver } from '../../snippets/variableResolve' import { UltiSnippetOption } from '../../types' import { getEnd } from '../../util/position' import workspace from '../../workspace' import helper from '../helper' let nvim: Neovim beforeAll(async () => { await helper.setup() nvim = helper.nvim let pyfile = path.join(__dirname, '../ultisnips.py') await nvim.command(`execute 'pyxfile '.fnameescape('${pyfile}')`) }) afterAll(async () => { await helper.shutdown() }) async function createSnippet(snippet: string | TextmateSnippet, opts?: UltiSnippetOption, range = Range.create(0, 0, 0, 0), line = '') { let resolver = new SnippetVariableResolver(nvim, workspace.workspaceFolderControl) let snip = new CocSnippet(snippet, Position.create(0, 0), nvim, resolver) let context: UltiSnippetContext if (opts) { context = { range, line, ...opts, id: generateContextId(workspace.bufnr) } await executePythonCode(nvim, getInitialPythonCode(context)) } await snip.init(context) return snip } describe('SnippetString', () => { it('should check SnippetString', () => { expect(SnippetString.isSnippetString(null)).toBe(false) let snippetString = new SnippetString() expect(SnippetString.isSnippetString(snippetString)).toBe(true) expect(SnippetString.isSnippetString({})).toBe(false) }) it('should build snippet string', () => { let snippetString: SnippetString snippetString = new SnippetString() assert.strictEqual(snippetString.appendText('I need $ and $').value, 'I need \\$ and \\$') snippetString = new SnippetString() assert.strictEqual(snippetString.appendText('I need \\$').value, 'I need \\\\\\$') snippetString = new SnippetString() snippetString.appendPlaceholder('fo$o}') assert.strictEqual(snippetString.value, '${1:fo\\$o\\}}') snippetString = new SnippetString() snippetString.appendText('foo').appendTabstop(0).appendText('bar') assert.strictEqual(snippetString.value, 'foo$0bar') snippetString = new SnippetString() snippetString.appendText('foo').appendTabstop().appendText('bar') assert.strictEqual(snippetString.value, 'foo$1bar') snippetString = new SnippetString() snippetString.appendText('foo').appendTabstop(42).appendText('bar') assert.strictEqual(snippetString.value, 'foo$42bar') snippetString = new SnippetString() snippetString.appendText('foo').appendPlaceholder('farboo').appendText('bar') assert.strictEqual(snippetString.value, 'foo${1:farboo}bar') snippetString = new SnippetString() snippetString.appendText('foo').appendPlaceholder('far$boo').appendText('bar') assert.strictEqual(snippetString.value, 'foo${1:far\\$boo}bar') snippetString = new SnippetString() snippetString.appendText('foo').appendPlaceholder(b => b.appendText('abc').appendPlaceholder('nested')).appendText('bar') assert.strictEqual(snippetString.value, 'foo${1:abc${2:nested}}bar') snippetString = new SnippetString() snippetString.appendVariable('foo', 'foo') assert.strictEqual(snippetString.value, '${foo:foo}') snippetString = new SnippetString() snippetString.appendText('foo').appendVariable('TM_SELECTED_TEXT').appendText('bar') assert.strictEqual(snippetString.value, 'foo${TM_SELECTED_TEXT}bar') snippetString = new SnippetString() snippetString.appendVariable('BAR', b => b.appendPlaceholder('ops')) assert.strictEqual(snippetString.value, '${BAR:${1:ops}}') snippetString = new SnippetString() snippetString.appendVariable('BAR', b => {}) assert.strictEqual(snippetString.value, '${BAR}') snippetString = new SnippetString() snippetString.appendChoice(['b', 'a', 'r']) assert.strictEqual(snippetString.value, '${1|b,a,r|}') snippetString = new SnippetString() snippetString.appendChoice(['b,1', 'a,2', 'r,3']) assert.strictEqual(snippetString.value, '${1|b\\,1,a\\,2,r\\,3|}') snippetString = new SnippetString() snippetString.appendChoice(['b', 'a', 'r'], 0) assert.strictEqual(snippetString.value, '${0|b,a,r|}') snippetString = new SnippetString() snippetString.appendText('foo').appendChoice(['far', 'boo']).appendText('bar') assert.strictEqual(snippetString.value, 'foo${1|far,boo|}bar') snippetString = new SnippetString() snippetString.appendText('foo').appendChoice(['far', '$boo']).appendText('bar') assert.strictEqual(snippetString.value, 'foo${1|far,$boo|}bar') snippetString = new SnippetString() snippetString.appendText('foo').appendPlaceholder('farboo').appendChoice(['far', 'boo']).appendText('bar') assert.strictEqual(snippetString.value, 'foo${1:farboo}${2|far,boo|}bar') }) it('should escape/apply snippet choices correctly', () => { { const s = new SnippetString() s.appendChoice(["aaa$aaa"]) s.appendText("bbb$bbb") assert.strictEqual(s.value, '${1|aaa$aaa|}bbb\\$bbb') } { const s = new SnippetString() s.appendChoice(["aaa,aaa"]) s.appendText("bbb$bbb") assert.strictEqual(s.value, '${1|aaa\\,aaa|}bbb\\$bbb') } { const s = new SnippetString() s.appendChoice(["aaa|aaa"]) s.appendText("bbb$bbb") assert.strictEqual(s.value, '${1|aaa\\|aaa|}bbb\\$bbb') } { const s = new SnippetString() s.appendChoice(["aaa\\aaa"]) s.appendText("bbb$bbb") assert.strictEqual(s.value, '${1|aaa\\\\aaa|}bbb\\$bbb') } }) }) describe('toSnippetString()', () => { it('should convert snippet to string', async () => { expect(() => { toSnippetString(1 as any) }).toThrow(TypeError) expect(toSnippetString(new SnippetString())).toBe('') }) }) describe('CocSnippet', () => { async function assertResult(snip: string, resolved: string, opts?: UltiSnippetOption) { let c = await createSnippet(snip, opts) expect(c.text).toBe(resolved) } async function assertPyxValue(code: string, res: any) { let val = await nvim.call(`pyxeval`, code) as string if (typeof res === 'number' || typeof res === 'string' || typeof res === 'boolean') { expect(val).toBe(res) } else if (res instanceof RegExp) { expect(val).toMatch(res) } else { expect(val).toEqual(res) } } describe('resolveVariables()', () => { it('should padZero', () => { expect(padZero(1)).toBe('01') expect(padZero(10)).toBe('10') }) it('should getVariablesCode', () => { expect(getVariablesCode({})).toBe('t = ()') expect(getVariablesCode({ 1: 'foo', 3: 'bar' })).toBe('t = ("","foo","","bar",)') }) it('should resolve uppercase variables', async () => { let doc = await helper.createDocument() let fsPath = URI.parse(doc.uri).fsPath await assertResult('$TM_FILENAME', path.basename(fsPath)) await assertResult('$TM_FILENAME_BASE', path.basename(fsPath, path.extname(fsPath))) await assertResult('$TM_DIRECTORY', path.dirname(fsPath)) await assertResult('$TM_FILEPATH', fsPath) await nvim.call('setreg', ['""', 'foo']) await assertResult('$YANK', 'foo') await assertResult('$TM_LINE_INDEX', '0') await assertResult('$TM_LINE_NUMBER', '1') await nvim.setLine('foo') await assertResult('$TM_CURRENT_LINE', 'foo') await nvim.call('setreg', ['*', 'foo']) await assertResult('$CLIPBOARD', 'foo') let d = new Date() await assertResult('$CURRENT_YEAR', d.getFullYear().toString()) await assertResult('$NOT_EXISTS', 'NOT_EXISTS') await assertResult('$TM_CURRENT_WORD', 'foo') }) it('should resolve new VSCode variables', async () => { let doc = await helper.createDocument() await doc.buffer.setOption('comments', 's1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-') await doc.buffer.setOption('commentstring', '') let fsPath = URI.parse(doc.uri).fsPath let c = await createSnippet('$RANDOM') expect(c.text.length).toBe(6) c = await createSnippet('$RANDOM_HEX') expect(c.text.length).toBe(6) c = await createSnippet('$UUID') expect(c.text).toMatch('-') c = await createSnippet('$RELATIVE_FILEPATH') expect(c.text).toMatch(path.basename(fsPath)) c = await createSnippet('$WORKSPACE_NAME') expect(c.text.length).toBeGreaterThan(0) c = await createSnippet('$WORKSPACE_FOLDER') expect(c.text.length).toBeGreaterThan(0) await assertResult('$LINE_COMMENT', '//') await assertResult('$BLOCK_COMMENT_START', '/*') await assertResult('$BLOCK_COMMENT_END', '*/') await doc.buffer.setOption('comments', '') await doc.buffer.setOption('commentstring', '// %s') await assertResult('$LINE_COMMENT', '//') await assertResult('$BLOCK_COMMENT_START', '') await assertResult('$BLOCK_COMMENT_END', '') }) it('should resolve variables in placeholders', async () => { await nvim.setLine('foo') await assertResult('$1 ${1:$TM_CURRENT_LINE}', 'foo foo') await assertResult('$1 ${1:$TM_CURRENT_LINE bar}', 'foo bar foo bar') await assertResult('$2 ${2:|${1:$TM_CURRENT_LINE}|}', '|foo| |foo|') await assertResult('$1 $2 ${2:${1:|$TM_CURRENT_LINE|}}', '|foo| |foo| |foo|') }) it('should resolve variables with default value', async () => { await assertResult('$1 ${1:${VISUAL:foo}}', 'foo foo') }) it('should resolve for lower case variables', async () => { await assertResult('${foo:abcdef} ${bar}', 'abcdef bar') await assertResult('${1:${foo:abcdef}} ${1/^\\w\\w(.*)/$1/}', 'abcdef cdef') }) }) describe('getUltiSnipOption', () => { it('should get snippets option', async () => { let c = await createSnippet('${1:foo}', { noExpand: true }) let m = c.tmSnippet.children[0] expect(c.getUltiSnipOption(m, 'noExpand')).toBe(true) expect(c.getUltiSnipOption(c.tmSnippet, 'noExpand')).toBe(true) expect(c.getUltiSnipOption(new Text(''), 'trimTrailingWhitespace')).toBeUndefined() }) }) describe('findParent()', () => { it('should throw when not found', async () => { let snip = new TextmateSnippet() snip.appendChild(new Text('f')) let c = await createSnippet(snip) expect(() => { c.findParent(Range.create(1, 0, 1, 0)) }).toThrow(Error) }) it('should not use adjacent choice placeholder', async () => { let c = await createSnippet('a\n${1|one,two,three|}\nb') let res = c.findParent(Range.create(1, 0, 1, 0)) expect(res.marker instanceof TextmateSnippet).toBe(true) }) }) describe('replaceWithText()', () => { it('should not return undefined when no change', async () => { let c = await createSnippet('${1:foo}') let token = (new CancellationTokenSource()).token let res = await c.replaceWithText(Range.create(0, 0, 0, 0), '', token) expect(res).toBeDefined() expect(res.snippetText).toBe('foo') }) it('should replace with Text for choice placeholder', async () => { let c = await createSnippet(' ${1|one,two,three|} ') let res = c.replaceWithMarker(Range.create(0, 2, 0, 4), new Text('bar')) expect(res.children.length).toBe(1) expect(res.children[0].toString()).toBe('obar') }) it('should not insert line break at the start of placeholder', async () => { let c = await createSnippet(' ${1:bar} ') let p = c.getPlaceholderByIndex(1).marker let res = c.replaceWithMarker(Range.create(0, 1, 0, 1), new Text('\n'), p) let text = c.tmSnippet.children[0] as Text expect(text.value).toBe(' \n') expect(res.toString()).toBe('bar') }) it('should return undefined when cursor not changed', async () => { let doc = await workspace.document let c = await createSnippet('${1:foo}') let token = (new CancellationTokenSource()).token let res = await c.replaceWithText(Range.create(0, 0, 0, 3), '', token, undefined, doc.cursor) expect(res.delta).toBeUndefined() }) it('should synchronize without related change', async () => { const assertChange = async (range: Range, newText: string, resultText: string) => { let token = (new CancellationTokenSource()).token let c = await createSnippet('begin ${1:foo} end') await c.replaceWithText(range, newText, token) expect(c.text).toBe(resultText) let start = Position.create(0, 0) let end = getEnd(start, resultText) expect(c.range).toEqual(Range.create(start, end)) return c } // insert text await assertChange(Range.create(0, 0, 0, 0), 'aa ', 'aa begin foo end') // insert placeholder let snippet = await assertChange(Range.create(0, 6, 0, 6), 'xx', 'begin xxfoo end') let p = snippet.getPlaceholderByIndex(1) expect(p.value).toBe('xxfoo') // delete text of placeholder snippet = await assertChange(Range.create(0, 6, 0, 9), '', 'begin end') p = snippet.getPlaceholderByIndex(1) expect(p.value).toBe('') // delete text await assertChange(Range.create(0, 0, 0, 6), '', 'foo end') // delete Text and Placeholder snippet = await assertChange(Range.create(0, 0, 0, 8), '', 'o end') p = snippet.getPlaceholderByIndex(1) expect(p).toBeUndefined() let marker = snippet.getPlaceholderById(0.5, 0) expect(marker).toBeDefined() marker = snippet.getPlaceholderById(10, 9) expect(marker).toBeUndefined() }) it('should prefer current placeholder', async () => { let m: Placeholder let c = await createSnippet('b ${1:${2:bar} foo} x') let marker = c.getPlaceholderByIndex(1).marker // use outer m = c.replaceWithMarker(Range.create(0, 2, 0, 3), new Text('insert'), marker) as Placeholder expect(m).toBe(marker) expect(m.children.length).toBe(1) expect(m.children[0].toString()).toBe('insertar foo') // use inner c = await createSnippet('b ${1:${2:bar} foo} x') m = c.replaceWithMarker(Range.create(0, 2, 0, 3), new Text('insert')) as Placeholder expect(m instanceof Placeholder).toBe(true) expect(m.index).toBe(2) expect(m.children.length).toBe(1) expect(m.children[0].toString()).toBe('insertar') }) it('should insert with marker', async () => { let c; let m c = await createSnippet('${1:foo} ${2:bar}') m = c.replaceWithMarker(Range.create(0, 0, 0, 0), new Text('before')) expect(m.toString()).toBe('beforefoo') expect(m.children.length).toBe(1) c = await createSnippet('${1:foo} ${2:bar}') m = c.replaceWithMarker(Range.create(0, 1, 0, 1), new Text('before')) expect(m.toString()).toBe('fbeforeoo') expect(m.children.length).toBe(1) c = await createSnippet('${1:foo} ${2:bar}') m = c.replaceWithMarker(Range.create(0, 3, 0, 3), new Text('before')) expect(m.toString()).toBe('foobefore') expect(m.children.length).toBe(1) }) it('should insert inside text', async () => { let c = await createSnippet('foo ${1:bar}') let marker = (new SnippetParser()).parse('${1:a}', true) let res = c.replaceWithMarker(Range.create(0, 1, 0, 2), marker) expect(res).toBe(c.tmSnippet) expect(c.tmSnippet.toString()).toBe('fao bar') }) it('should change final placeholder', async () => { let c = await createSnippet('${1:foo} ${0:bar}') let changed = c.replaceWithMarker(Range.create(0, 4, 0, 4), new Text(' ')) expect(changed.toString()).toBe('foo bar') c.synchronize() changed = c.replaceWithMarker(Range.create(0, 5, 0, 6), new Text('')) expect(changed['index']).toBe(0) expect(changed.toString()).toBe('ar') }) it('should replace with Text when placeholder is not primary', async () => { let c = await createSnippet('$1 ${1:foo}') let result = await c.replaceWithText(Range.create(0, 0, 0, 1), 'b', CancellationToken.None) expect(result.marker instanceof Text).toBe(true) expect(result.snippetText).toBe('boo foo') }) }) describe('replaceWithSnippet()', () => { it('should insert nested placeholder', async () => { let c = await createSnippet('${1:foo}\n$1', {}) c.deactivateSnippet(undefined) // expect(c.getUltiSnipActionCodes(undefined, 'postJump')).toBeUndefined() let res = await c.replaceWithSnippet(Range.create(0, 0, 0, 3), '${1:bar}') expect(res.toString()).toBe('bar') expect(res.parent.snippet.toString()).toBe('bar\nbar') expect(c.text).toBe('bar\nbar') }) it('should insert python snippet to normal snippet', async () => { let c = await createSnippet('${1:foo}\n$1', {}) let p = c.getPlaceholderByIndex(1) expect(c.hasPython).toBe(false) let res = await c.replaceWithSnippet(p.range, '${1:x} `!p snip.rv = t[1]`', p.marker, { line: '', range: p.range, id: `1-1` }) expect(res.toString()).toBe('x x') expect(c.text).toBe('x x\nx x') let r = c.getPlaceholderByMarker(res.first) let source = new CancellationTokenSource() let result = await c.replaceWithText(r.range, 'bar', source.token) expect(result.snippetText).toBe('bar x\nx x') expect(c.text).toBe('bar bar\nbar bar') expect(c.hasPython).toBe(true) }) it('should not change match for original placeholders', async () => { let c = await createSnippet('`!p snip.rv = match.group(1)` $1', { regex: '^(\\w+)' }, Range.create(0, 0, 0, 3), 'foo') let p = c.getPlaceholderByIndex(1) expect(c.hasPython).toBe(true) expect(c.text).toBe('foo ') let context = { id: `1-1`, regex: '^(\\w+)', line: 'bar', range: Range.create(0, 0, 0, 3) } await executePythonCode(nvim, getInitialPythonCode(context)) await c.replaceWithSnippet(p.range, '`!p snip.rv = match.group(1)`', p.marker, context) expect(c.text).toBe('foo bar') }) it('should update with independent python global', async () => { let c = await createSnippet('${1:foo} `!p snip.rv = t[1]`', {}) let range = Range.create(0, 0, 0, 3) let line = await nvim.line await c.replaceWithSnippet(range, '${1:bar} `!p snip.rv = t[1]`', undefined, { range, line, id: `1-1` }) expect(c.text).toBe('bar bar bar bar') let token = (new CancellationTokenSource()).token let res = await c.replaceWithText(Range.create(0, 0, 0, 3), 'xy', token) expect(c.text).toBe('xy xy xy xy') expect(res.delta).toBeUndefined() }) it('should not throw when parent not exist', async () => { let c = await createSnippet('${1:foo}', {}) await c.onMarkerUpdate(new Placeholder(1), CancellationToken.None) }) it('should not synchronize with none primary placeholder change', async () => { let c = await createSnippet('${1:foo}\n$1', {}) let res = await c.replaceWithSnippet(Range.create(1, 0, 1, 3), '${1:bar}') expect(res.toString()).toBe('bar') expect(c.tmSnippet.toString()).toBe('foo\nbar') }) }) describe('getMarkerPosition', () => { it('should get position of marker', async () => { let c = await createSnippet('${1:foo}') expect(c.getMarkerPosition(new Placeholder(1))).toBeUndefined() let cloned = c.tmSnippet.clone() expect(c.getMarkerPosition(cloned)).toBeUndefined() expect(c.getMarkerPosition(c.tmSnippet)).toBeDefined() }) }) describe('code block initialize', () => { it('should init shell code block', async () => { await assertResult('`echo "hello"` world', 'hello world', {}) }) it('should init vim block', async () => { await assertResult('`!v eval("1 + 1")` = 2', '2 = 2', {}) await nvim.setLine(' ') await assertResult('${1:`!v indent(".")`} "$1"', '2 "2"', {}) }) it('should init code block in placeholders', async () => { await assertResult('f ${1:`echo "b"`}', 'f b', {}) await assertResult('f ${1:`!v "b"`}', 'f b', {}) await assertResult('f ${1:`!p snip.rv = "b"`}', 'f b', {}) }) it('should setup python globals', async () => { await helper.edit('t.js') await createSnippet('`!p snip.rv = fn`', {}) await assertPyxValue('fn', 't.js') await assertPyxValue('path', /t\.js$/) await assertPyxValue('t', ['']) await createSnippet('`!p snip.rv = fn`', { regex: '[ab]', context: 'False' }, Range.create(0, 2, 0, 3), 'a b') await assertPyxValue('match.group(0)', 'b') }) it('should setup python match', async () => { let c = await createSnippet('\\\\frac{`!p snip.rv = match.group(1)`}{$1}$0', { regex: '((\\d+)|(\\d*)(\\\\)?([A-Za-z]+)((\\^|_)(\\{\\d+\\}|\\d))*)/', context: 'True' }, Range.create(0, 0, 0, 3), '20/') await assertPyxValue('match.group(1)', '20') expect(c.text).toBe('\\frac{20}{}') }) it('should work with methods of snip', async () => { await nvim.command('setl shiftwidth=4 ft=txt tabstop=4 expandtab') await createSnippet('`!p snip.rv = "a"`', {}, Range.create(0, 4, 0, 8), ' abcd') await executePythonCode(nvim, []) await executePythonCode(nvim, [ 'snip.shift(1)', // ultisnip indent only when there's '\n' in snip.rv 'snip += ""', 'newLine = snip.mkline("foo")' ]) await assertPyxValue('newLine', ' foo') await executePythonCode(nvim, [ 'snip.unshift(1)', 'newLine = snip.mkline("b")' ]) await assertPyxValue('newLine', ' b') await executePythonCode(nvim, [ 'snip.shift(1)', 'snip.reset_indent()', 'newLine = snip.mkline("f")' ]) await assertPyxValue('newLine', ' f') await executePythonCode(nvim, [ 'fff = snip.opt("&fff", "foo")', 'ft = snip.opt("&ft", "ft")', ]) await assertPyxValue('fff', 'foo') await assertPyxValue('ft', 'txt') }) it('should init python code block', async () => { await assertResult('`!p snip.rv = "a"` = a', 'a = a', {}) await assertResult('`!p snip.rv = t[1]` = ${1:a}', 'a = a', {}) await assertResult('`!p snip.rv = t[1]` = ${1:`!v eval("\'a\'")`}', 'a = a', {}) await assertResult('`!p snip.rv = t[1] + t[2]` = ${1:a} ${2:b}', 'ab = a b', {}) }) it('should init python placeholder', async () => { await assertResult('foo ${1/^\\|(.*)\\|$/$1/} ${1:|`!p snip.rv = "a"`|}', 'foo a |a|', {}) await assertResult('foo $1 ${1:`!p snip.rv = "a"`}', 'foo a a', {}) await assertResult('${1/^_(.*)/$1/} $1 aa ${1:`!p snip.rv = "_foo"`}', 'foo _foo aa _foo', {}) }) it('should init nested python placeholder', async () => { await assertResult('${1:foo`!p snip.rv = t[2]`} ${2:bar} $1', 'foobar bar foobar', {}) await assertResult('${3:f${2:oo${1:b`!p snip.rv = "ar"`}}} `!p snip.rv = t[3]`', 'foobar foobar', {}) }) it('should recursive init python placeholder', async () => { await assertResult('${1:`!p snip.rv = t[2]`} ${2:`!p snip.rv = t[3]`} ${3:`!p snip.rv = t[4][0]`} ${4:bar}', 'b b b bar', {}) await assertResult('${1:foo} ${2:`!p snip.rv = t[1][0]`} ${3:`!p snip.rv = ""`} ${4:`!p snip.rv = t[2]`}', 'foo f f', {}) }) it('should update python block from placeholder', async () => { await assertResult('`!p snip.rv = t[1][0] if len(t[1]) > 0 else ""` ${1:`!p snip.rv = t[2]`} ${2:foo}', 'f foo foo', {}) }) }) describe('updatePlaceholder()', () => { async function assertUpdate(text: string, value: string, result: string, index = 1, ultisnip: UltiSnippetOption | null = {}): Promise { let c = await createSnippet(text, ultisnip) let p = c.getPlaceholderByIndex(index) expect(p != null).toBe(true) p.marker.setOnlyChild(new Text(value)) await c.tmSnippet.update(nvim, p.marker, CancellationToken.None) expect(c.tmSnippet.toString()).toBe(result) return c } it('should update variable placeholders', async () => { await assertUpdate('${foo} ${foo}', 'bar', 'bar bar', 1, null) await assertUpdate('${1:${foo:x}} $1', 'bar', 'bar bar', 1, null) }) it('should not update when cancelled', async () => { let c = await createSnippet('${1:foo} `!p snip.rv = t[1]`', {}) let p = c.getPlaceholderByIndex(1) expect(p != null).toBe(true) p.marker.setOnlyChild(new Text('bar')) await c.tmSnippet.update(nvim, p.marker, CancellationToken.Cancelled) expect(c.tmSnippet.toString()).toBe('bar foo') }) it('should work with snip.c', async () => { let code = [ '#ifndef ${1:`!p', 'if not snip.c:', ' import random, string', " name = re.sub(r'[^A-Za-z0-9]+','_', snip.fn).upper()", " rand = ''.join(random.sample(string.ascii_letters+string.digits, 8))", " snip.rv = ('%s_%s' % (name,rand)).upper()", "else:", " snip.rv = snip.c + t[2]`}", '#define $1', '$2' ].join('\n') let c = await createSnippet(code, {}) let first = c.text.split('\n')[0] let p = c.getPlaceholderByIndex(2) expect(p).toBeDefined() p.marker.setOnlyChild(new Text('foo')) await c.tmSnippet.update(nvim, p.marker, CancellationToken.None) let t = c.tmSnippet.toString() expect(t.startsWith(first)).toBe(true) expect(t.split('\n').map(s => s.endsWith('foo'))).toEqual([true, true, true]) }) it('should update placeholder with code blocks', async () => { await assertUpdate('${1:`echo "foo"`} $1', 'bar', 'bar bar') await assertUpdate('${2:${1:`echo "foo"`}} $2', 'bar', 'bar bar') await assertUpdate('${1:`!v "foo"`} $1', 'bar', 'bar bar') await assertUpdate('${1:`!p snip.rv = "foo"`} $1', 'bar', 'bar bar') }) it('should update related python blocks', async () => { // multiple await assertUpdate('`!p snip.rv = t[1]` ${1:`!p snip.rv = "foo"`} `!p snip.rv = t[1]`', 'bar', 'bar bar bar') // parent await assertUpdate('`!p snip.rv = t[2]` ${2:foo ${1:`!p snip.rv = "foo"`}}', 'bar', 'foo bar foo bar') // related placeholders await assertUpdate('${2:foo `!p snip.rv = t[1]`} ${1:`!p snip.rv = "foo"`}', 'bar', 'foo bar bar') }) it('should update python code blocks with normal placeholder values', async () => { await assertUpdate('`!p snip.rv = t[1]` $1 `!p snip.rv = t[1]`', 'bar', 'bar bar bar') await assertUpdate('`!p snip.rv = t[2]` ${2:foo $1}', 'bar', 'foo bar foo bar') await assertUpdate('${2:foo `!p snip.rv = t[1]`} $1', 'bar', 'foo bar bar') }) it('should reset values for removed placeholders', async () => { // Keep remained placeholder this is same behavior of VSCode. let s = await assertUpdate('${2:bar${1:foo}} $2 $1', 'bar', 'bar bar foo', 2) let p = s.getPlaceholderByIndex(2).marker let marker = getNextPlaceholder(p, false) let prev = s.getPlaceholderByMarker(marker) expect(prev).toBeDefined() expect(prev.value).toBe('foo') // python placeholder, reset to empty value await assertUpdate('${2:bar${1:foo}} $2 `!p snip.rv = t[1]`', 'bar', 'bar bar ', 2) // not reset since $1 still exists await assertUpdate('${2:bar${1:foo}} $2 $1 `!p snip.rv = t[1]`', 'bar', 'bar bar foo foo', 2) }) }) describe('getNextPlaceholder()', () => { it('should get next placeholder', async () => { let c = await createSnippet('${1:a} ${2:b}') let p = c.getPlaceholderByIndex(1) let nested = await c.replaceWithSnippet(p.range, '${1:foo} ${2:bar}') nested.placeholders.forEach(p => { p.primary = false }) let snip = c.snippets[1] expect(c.snippets[1]).toBe(nested) let marker = snip.first let next = getNextPlaceholder(marker, true) expect(next.index).toBe(2) expect(next.toString()).toBe('bar') { let m = nested.placeholders.find(o => o.index === 0) let next = getNextPlaceholder(m, false) expect(next.toString()).toBe('foo bar') } }) it('should not throw when next not exists', async () => { expect(getNextPlaceholder(new Placeholder(1), true)).toBeUndefined() expect(getNextPlaceholder(undefined, true)).toBeUndefined() }) it('should not throw when next not exists', async () => { expect(getNextPlaceholder(new Placeholder(1), true)).toBeUndefined() expect(getNextPlaceholder(undefined, true)).toBeUndefined() }) it('should prefer primary placeholder', async () => { let c = await createSnippet('$1 $2 ${1:foo}') let p = c.getPlaceholderByIndex(2) let next = getNextPlaceholder(p.marker, false) expect(next.index).toBe(1) expect(next.primary).toBe(true) }) }) describe('getUltiSnipActionCodes()', () => { it('should not get codes when action not exists', () => { expect(getUltiSnipActionCodes(undefined, 'postJump')).toBeUndefined() expect(getUltiSnipActionCodes(new Text(''), 'postJump')).toBeUndefined() let snip = (new SnippetParser()).parse('${1:a}', true) expect(getUltiSnipActionCodes(snip, 'postJump')).toBeUndefined() }) it('should get codes when exists action', async () => { let snip = (new SnippetParser()).parse('${1:a}', true) snip.related.context = { id: `1-1`, line: '', range: Range.create(0, 0, 0, 0), actions: { postJump: 'jump' } } let res = getUltiSnipActionCodes(snip, 'postJump') expect(res.length).toBe(2) }) }) describe('getRanges getSnippetPlaceholders getTabStops', () => { it('should get ranges of placeholder', async () => { let c = await createSnippet('${2:${1:x} $1}\n$2', {}) let p = c.getPlaceholderByIndex(1) let arr = c.getRanges(p.marker) expect(arr.length).toBe(2) expect(arr[0]).toEqual(Range.create(0, 0, 0, 1)) expect(arr[1]).toEqual(Range.create(0, 2, 0, 3)) expect(c.text).toBe('x x\nx x') }) it('should get range of marker snippet', async () => { let c = await createSnippet('${1:foo}', {}) let p = new Placeholder(1) expect(c.getSnippetRange(p)).toBeUndefined() let snip = (new SnippetParser()).parse('${1:a}', true) expect(c.getSnippetRange(snip.children[0])).toBeUndefined() let range = c.getSnippetRange(c.tmSnippet.children[0]) expect(range).toEqual(Range.create(0, 0, 0, 3)) }) it('should get snippet tabstops', async () => { let c = await createSnippet('${1:foo}', {}) let p = new Placeholder(1) expect(c.getSnippetTabstops(p)).toEqual([]) let tabstops = c.getSnippetTabstops(c.tmSnippet.children[0]) expect(tabstops.length).toBe(2) }) }) describe('utils', () => { function assertThrow(fn: () => void) { let err try { fn() } catch (e) { err = e } expect(err).toBeDefined() } it('should getTextBefore', () => { function assertText(r: number[], text: string, pos: [number, number], res: string): void { let t = getTextBefore(Range.create(r[0], r[1], r[2], r[3]), text, Position.create(pos[0], pos[1])) expect(t).toBe(res) } assertText([1, 1, 2, 1], 'abc\nd', [1, 1], '') assertText([1, 1, 2, 1], 'abc\nd', [2, 1], 'abc\nd') assertText([1, 1, 3, 1], 'abc\n\nd ', [3, 1], 'abc\n\nd') }) it('should getTextAfter', () => { function assertText(r: number[], text: string, pos: [number, number], res: string): void { let t = getTextAfter(Range.create(r[0], r[1], r[2], r[3]), text, Position.create(pos[0], pos[1])) expect(t).toBe(res) } assertText([1, 1, 2, 1], 'abc\nd', [1, 1], 'abc\nd') assertText([1, 1, 2, 1], 'abc\nd', [2, 1], '') assertText([1, 1, 3, 1], 'abc\n\nd', [2, 0], '\nd') assertText([0, 0, 0, 3], 'abc', [0, 3], '') }) it('should check shouldFormat', () => { expect(shouldFormat(' f')).toBe(true) expect(shouldFormat('a\nb')).toBe(true) expect(shouldFormat('foo')).toBe(false) }) it('should normalizeSnippetString', () => { expect(normalizeSnippetString('a\n\n\tb', ' ', { insertSpaces: true, trimTrailingWhitespace: true, tabSize: 2 })).toBe('a\n\n b') expect(normalizeSnippetString('a\n\n b', '\t', { insertSpaces: false, trimTrailingWhitespace: true, tabSize: 2 })).toBe('a\n\n\t\tb') let res = normalizeSnippetString('a\n\n\tb', '\t', { insertSpaces: false, trimTrailingWhitespace: false, noExpand: true, tabSize: 2 }) expect(res).toBe('a\n\t\n\t\tb') }) it('should throw for invalid regex', async () => { assertThrow(() => { convertRegex('\\z') }) assertThrow(() => { convertRegex('(?s)') }) assertThrow(() => { convertRegex('(?x)') }) assertThrow(() => { convertRegex('a\nb') }) assertThrow(() => { convertRegex('(<)?(\\w+@\\w+(?:\\.\\w+)+)(?(1)>|$)') }) assertThrow(() => { convertRegex('(<)?(\\w+@\\w+(?:\\.\\w+)+)(?(1)>|)') }) }) it('should convert regex', async () => { // \\A expect(convertRegex('\\A')).toBe('^') expect(convertRegex('f(?#abc)b')).toBe('fb') expect(convertRegex('f(?Pdef)b')).toBe('f(?def)b') expect(convertRegex('f(?P=abc)b')).toBe('f\\kb') }) it('should catch error with executePythonCode', async () => { let fn = async () => { await executePythonCode(nvim, ['INVALID_CODE']) } await expect(fn()).rejects.toThrow(Error) }) it('should set error with addPythonTryCatch', async () => { let code = addPythonTryCatch('INVALID_CODE', true) await nvim.command(`pyx ${code}`) let msg = await nvim.getVar('errmsg') expect(msg).toBeDefined() expect(msg).toMatch('INVALID_CODE') }) it('should cancel code block eval when necessary', async (): Promise => { { let block = new CodeBlock('echo "foo"', 'shell') await block.resolve(nvim, CancellationToken.Cancelled) expect(block.len()).toBe(0) } { let block = new CodeBlock('bufnr("%")', 'vim') await block.resolve(nvim, CancellationToken.None) let bufnr = await nvim.eval('bufnr("%")') expect(block.value).toBe(`${bufnr}`) } { let block = new CodeBlock('v:null', 'vim') await block.resolve(nvim) expect(block.value).toBe('') } { await executePythonCode(nvim, [`snip = SnippetUtil("", (0, 0), (0, 0), None)`]) let block = new CodeBlock('snip.rv = "foo"', 'python') let tokenSource = new CancellationTokenSource() let token = tokenSource.token process.nextTick(() => { tokenSource.cancel() }) await block.resolve(nvim, token) } }) it('should parse comments', async () => { expect(parseCommentstring('a%sb')).toBeUndefined() expect(parseCommentstring('// %s')).toBe('//') expect(parseComments('')).toEqual({ start: undefined, end: undefined, single: undefined }) expect(parseComments('s:/*')).toEqual({ start: '/*', end: undefined, single: undefined }) expect(parseComments('e:*/')).toEqual({ end: '*/', start: undefined, single: undefined }) expect(parseComments(':#,:b')).toEqual({ end: undefined, start: undefined, single: '#' }) }) it('should set request variable', async () => { events.requesting = true await executePythonCode(nvim, ['stat = __requesting']) let res = await nvim.call('pyxeval', ['stat']) expect(res).toBe(true) events.requesting = false await executePythonCode(nvim, ['stat = __requesting']) res = await nvim.call('pyxeval', ['stat']) expect(res).toBe(false) }) it('should check hasPython', () => { expect(hasPython(undefined)).toBe(false) expect(hasPython({ context: 'context' })).toBe(true) }) }) }) ================================================ FILE: src/__tests__/tree/basicProvider.test.ts ================================================ import { CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol' import commandsManager from '../../commands' import { TreeItemCollapsibleState } from '../../tree' import { HistoryInput } from '../../tree/filter' import BasicDataProvider, { TreeNode } from '../../tree/BasicDataProvider' import { disposeAll } from '../../util' let disposables: Disposable[] = [] type NodeDef = [string, NodeDef[]?] interface CustomNode extends TreeNode { kind?: string x?: number y?: number } afterEach(async () => { disposeAll(disposables) disposables = [] }) function createNode(label: string, children?: TreeNode[], key?: string, tooltip?: string): CustomNode { let res: TreeNode = { label } if (children) res.children = children if (tooltip) res.tooltip = tooltip if (key) res.key = key return res } let defaultDef: NodeDef[] = [ ['a', [['c'], ['d']]], ['b', [['e'], ['f']]], ['g'] ] function createLabels(data: ReadonlyArray): string[] { let res: string[] = [] const addLabels = (n: TreeNode, level: number) => { res.push(' '.repeat(level) + n.label) if (n.children) { for (let node of n.children) { addLabels(node, level + 1) } } } for (let item of data || []) { addLabels(item, 0) } return res } function findNode(label: string, nodes: ReadonlyArray): TreeNode | undefined { for (let n of nodes) { if (n.label == label) { return n } let children = n.children if (Array.isArray(children)) { let find = findNode(label, children) if (find) return find } } } export function createNodes(defs: NodeDef[]): TreeNode[] { return defs.map(o => { let children if (Array.isArray(o[1])) { children = createNodes(o[1]) } return createNode(o[0], children) }) } describe('HistoryInput()', () => { it('should manage history inputs', async () => { let h = new HistoryInput() h.add('a') h.add('b') expect(h.next('')).toBe('b') expect(h.next('a')).toBe('b') expect(h.next('b')).toBe('a') expect(h.toJSON()).toBe(`[b,a]`) expect(h.previous('')).toBe('a') expect(h.previous('a')).toBe('b') expect(h.previous('b')).toBe('a') }) }) describe('BasicDataProvider', () => { describe('getChildren()', () => { it('should get children from root', async () => { let nodes = createNodes(defaultDef) let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) let res = await provider.getChildren() expect(res.length).toBe(3) expect(res.map(o => o.label)).toEqual(['a', 'b', 'g']) }) it('should throw when result is not array', async () => { let provider = new BasicDataProvider({ provideData: () => { return undefined } }) disposables.push(provider) await expect(async () => { await provider.getChildren() }).rejects.toThrow(Error) expect(provider.getLevel(undefined)).toBe(0) }) it('should get children from child node', async () => { let provider = new BasicDataProvider({ provideData: () => { return createNodes(defaultDef) } }) disposables.push(provider) let res = await provider.getChildren() let nodes = await provider.getChildren(res[0]) expect(nodes.length).toBe(2) expect(nodes.map(o => o.label)).toEqual(['c', 'd']) }) it('should throw when provideData throws', async () => { let provider = new BasicDataProvider({ provideData: () => { throw new Error('my error') } }) disposables.push(provider) let err try { await provider.getChildren() } catch (e) { err = e } expect(err).toBeDefined() }) }) describe('getTreeItem()', () => { it('should get tree item from node', async () => { let provider = new BasicDataProvider({ provideData: () => { return createNodes(defaultDef) } }) disposables.push(provider) let res = await provider.getChildren() let item = provider.getTreeItem(res[0]) expect(item).toBeDefined() expect(item.collapsibleState).toBe(TreeItemCollapsibleState.Collapsed) item = provider.getTreeItem(res[2]) expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None) }) it('should respect expandLevel option', async () => { let def: NodeDef[] = [ ['a', [['c', [['e'], ['f']]], ['d']]], ['b'] ] let provider = new BasicDataProvider({ expandLevel: 1, provideData: () => { return createNodes(def) } }) disposables.push(provider) let res = await provider.getChildren() let item = provider.getTreeItem(res[0]) expect(item).toBeDefined() expect(item.collapsibleState).toBe(TreeItemCollapsibleState.Expanded) item = provider.getTreeItem(res[0].children[0]) expect(item.collapsibleState).toBe(TreeItemCollapsibleState.Collapsed) let n = 0 provider.iterate(res[0], undefined, 0, () => { n++ return true }) expect(n).toBe(5) }) it('should include highlights', async () => { let provider = new BasicDataProvider({ provideData: () => { return [createNode('a', [], undefined, 'tip')] } }) disposables.push(provider) let res = await provider.getChildren() let item = provider.getTreeItem(res[0]) expect(item).toBeDefined() expect(item.tooltip).toBe('tip') }) it('should use icon from node', async () => { let node = createNode('a', [], undefined, 'tip') node.icon = { text: 'i', hlGroup: 'Function' } let provider = new BasicDataProvider({ provideData: () => { return [node] } }) disposables.push(provider) let res = await provider.getChildren() let item = provider.getTreeItem(res[0]) expect(item).toBeDefined() expect(item.icon).toBeDefined() expect(item.icon).toEqual({ text: 'i', hlGroup: 'Function' }) }) it('should resolve icon', async () => { let provider = new BasicDataProvider({ provideData: () => { let node = createNode('a', [], undefined, 'tip') node.kind = 'function' return [node] }, resolveIcon: item => { if (item.kind === 'function') { return { text: 'f', hlGroup: 'Function' } } } }) disposables.push(provider) let res = await provider.getChildren() let item = provider.getTreeItem(res[0]) expect(item).toBeDefined() expect(item.icon).toEqual({ text: 'f', hlGroup: 'Function' }) }) }) describe('getParent()', () => { it('should get undefined when data does not exist', async () => { let node = createNode('a') let provider = new BasicDataProvider({ provideData: () => { return [node] } }) disposables.push(provider) let res = provider.getParent(node) expect(res).toBeUndefined() }) it('should get parent node', async () => { let node = createNode('g') let provider = new BasicDataProvider({ provideData: () => { return [ createNode('a', [createNode('c', [node]), createNode('d')]), createNode('b', [createNode('e'), createNode('f')]), createNode('g') ] } }) disposables.push(provider) await provider.getChildren() let res = provider.getParent(node) expect(res).toBeDefined() expect(res.label).toBe('c') // console.log(provider.labels.join('\n')) }) }) describe('resolveTreeItem()', () => { it('should resolve tooltip and command', async () => { let node = createNode('a') let provider = new BasicDataProvider({ provideData: () => { return [node] }, resolveItem: item => { item.tooltip = 'tip' item.command = { command: 'test command', title: 'test' } return item } }) disposables.push(provider) await provider.getChildren() let source = new CancellationTokenSource() let item = provider.getTreeItem(node) let resolved = await provider.resolveTreeItem(item, node, source.token) expect(resolved.tooltip).toBe('tip') expect(resolved.command.command).toBe('test command') }) it('should register command invoke click', async () => { let node = createNode('a') let called: TreeNode let provider = new BasicDataProvider({ provideData: () => { return [node] }, handleClick: item => { called = item } }) disposables.push(provider) await provider.getChildren() let source = new CancellationTokenSource() let item = provider.getTreeItem(node) let resolved = await provider.resolveTreeItem(item, node, source.token) expect(resolved.command).toBeDefined() expect(resolved.command.command).toMatch('invoke') await commandsManager.execute(resolved.command) expect(called).toBeDefined() expect(called).toBe(node) }) }) describe('update()', () => { it('should add children with event', async () => { let defs: NodeDef[] = [ ['a', [['b']]], ['b', [['f']]] ] let nodes = createNodes(defs) let b = nodes[0].children[0] let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let called = false provider.onDidChangeTreeData(node => { expect(node).toBe(b) called = true }) let newDefs: NodeDef[] = [ ['a', [['b', [['c'], ['d']]]]], ['b', [['f']]] ] let curr = provider.update(createNodes(newDefs)) let labels = createLabels(curr) expect(labels).toEqual([ 'a', ' b', ' c', ' d', 'b', ' f' ]) expect(called).toBe(true) expect(b.children).toBeDefined() expect(b.children.length).toBe(2) }) it('should remove children with event', async () => { let defs: NodeDef[] = [ ['a', [['b', [['c'], ['d']]]]], ['e', [['f']]] ] let nodes = createNodes(defs) let b = nodes[0].children[0] let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let called = false provider.onDidChangeTreeData(node => { expect(node).toBe(b) called = true }) let newDefs: NodeDef[] = [ ['a', [['b']]], ['e', [['f']]] ] let curr = provider.update(createNodes(newDefs)) let labels = createLabels(curr) expect(labels).toEqual([ 'a', ' b', 'e', ' f' ]) expect(called).toBe(true) expect(b.children).toBeUndefined() }) it('should not fire event for children when parent have changed', async () => { let defs: NodeDef[] = [ ['a', [['b', [['c'], ['d']]]]] ] let nodes = createNodes(defs) let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let called = 0 provider.onDidChangeTreeData(node => { expect(node).toBeUndefined() called += 1 }) let newDefs: NodeDef[] = [ ['a', [['b', [['c'], ['d'], ['g']]]]], ['e', [['f']]] ] let curr = provider.update(createNodes(newDefs)) expect(called).toBe(1) let labels = createLabels(curr) expect(labels).toEqual([ 'a', ' b', ' c', ' d', ' g', 'e', ' f' ]) }) it('should fire events for independent node change', async () => { let defs: NodeDef[] = [ ['a', [['b', [['c']]]]], ['e', [['f']]] ] let nodes = createNodes(defs) let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let called = [] provider.onDidChangeTreeData(node => { called.push(node) }) let newDefs: NodeDef[] = [ ['a', [['b', [['c'], ['d']]]]], ['e', [['f', [['g']]]]] ] let curr = provider.update(createNodes(newDefs)) expect(called.length).toBe(2) expect(called[0].label).toBe('b') expect(called[1].label).toBe('f') let labels = createLabels(curr) expect(labels).toEqual([ 'a', ' b', ' c', ' d', 'e', ' f', ' g' ]) }) it('should apply new properties', async () => { let defs: NodeDef[] = [ ['a', [['b']]], ['e', [['f']]] ] let nodes = createNodes(defs) let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let newNodes = createNodes([ ['a', [['b', [['c']]]]], ['e', [['f', [['g']]]]] ]) let b = newNodes[0].children[0] Object.assign(b, { x: 1, y: 2 }) let curr = provider.update(newNodes) let node = curr[0].children[0] expect(node).toBeDefined() expect(node.x).toBe(1) expect(node.y).toBe(2) }) it('should keep references and have new data sequence', async () => { let defs: NodeDef[] = [ ['a', [['b'], ['c']]], ['e', [['f']]], ['g'] ] let nodes = createNodes(defs) let keeps = [ findNode('a', nodes), findNode('b', nodes), findNode('c', nodes), findNode('e', nodes), findNode('f', nodes), ] let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let newNodes = createNodes([ ['a', [['c', [['d'], ['h']]], ['b']]], ['e', [['f', [['j']]], ['i']]] ]) let curr = provider.update(newNodes) expect(curr).toBe(nodes) expect(keeps[0]).toBe(findNode('a', curr)) expect(keeps[1]).toBe(findNode('b', curr)) expect(keeps[2]).toBe(findNode('c', curr)) expect(keeps[3]).toBe(findNode('e', curr)) expect(keeps[4]).toBe(findNode('f', curr)) let labels = createLabels(curr) expect(labels).toEqual([ 'a', ' c', ' d', ' h', ' b', 'e', ' f', ' j', ' i' ]) }) it('should use key for nodes', async () => { let nodes = [ createNode('a', [], 'x'), createNode('a', [], 'y'), createNode('a', [], 'z'), ] let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let newNodes = [ createNode('a', [], 'x'), createNode('a', [], 'z'), ] let curr = provider.update(newNodes) expect(curr.length).toBe(2) expect(curr[0].key).toBe('x') expect(curr[1].key).toBe('z') }) it('should reset data', async () => { let nodes = [ createNode('a', [], 'x'), ] let provider = new BasicDataProvider({ provideData: () => { return nodes } }) disposables.push(provider) await provider.getChildren() let newNodes = [ createNode('a', [], 'x'), ] let curr = provider.update(newNodes, true) expect(curr === nodes).toBe(false) }) }) describe('dispose', () => { it('should invoke onDispose from opts', async () => { let called = false let provider = new BasicDataProvider({ provideData: () => { return [] }, onDispose: () => { called = true } }) provider.dispose() expect(called).toBe(true) }) }) }) ================================================ FILE: src/__tests__/tree/treeView.test.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { Disposable } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import events from '../../events' import { ProviderResult } from '../../provider' import { TreeDataProvider, TreeViewOptions } from '../../tree' import BasicDataProvider, { ProviderOptions, TreeNode } from '../../tree/BasicDataProvider' import { getItemLabel, TreeItem, TreeItemCollapsibleState } from '../../tree/TreeItem' import TreeView from '../../tree/TreeView' import { disposeAll } from '../../util' import workspace from '../../workspace' import helper from '../helper' import { createNodes } from './basicProvider.test' type NodeDef = [string, NodeDef[]?] let nvim: Neovim let disposables: Disposable[] = [] let treeView: TreeView let provider: BasicDataProvider let nodes: TreeNode[] beforeAll(async () => { await helper.setup() nvim = helper.nvim }) afterAll(async () => { await helper.shutdown() }) beforeEach(async () => { await helper.createDocument() }) afterEach(async () => { if (provider) provider.dispose() if (treeView) treeView.dispose() disposeAll(disposables) await helper.reset() }) function createNode(label: string, children?: TreeNode[], key?: string, tooltip?: string): TreeNode { let res: TreeNode = { label } if (children) res.children = children if (tooltip) res.tooltip = tooltip if (key) res.key = key return res } function createTreeView(defs: NodeDef[], opts: Partial> = {}, providerOpts: Partial> = {}) { nodes = createNodes(defs) provider = new BasicDataProvider(Object.assign(providerOpts, { provideData: () => { return nodes } })) treeView = new TreeView('test', Object.assign(opts, { bufhidden: 'hide', treeDataProvider: provider })) } function updateData(defs: NodeDef[], reset = false) { nodes = createNodes(defs) provider.update(nodes, reset) } function makeUpdateUIThrowError() { let spy = jest.spyOn(treeView as any, 'updateUI').mockImplementation(() => { throw new Error('Test error') }) disposables.push(Disposable.create(() => { spy.mockRestore() })) } let defaultDef: NodeDef[] = [ ['a', [['c'], ['d']]], ['b', [['e'], ['f']]], ['g'] ] async function checkLines(arr: string[]): Promise { await helper.waitValue(async () => { return await nvim.call('getline', [1, '$']) }, arr) } describe('TreeView', () => { describe('TreeItem()', () => { it('should create TreeItem from resourceUri', async () => { let item = new TreeItem(URI.file('/foo/bar.ts')) expect(item.resourceUri).toBeDefined() expect(item.label).toBe('bar.ts') expect(item.label).toBeDefined() }) it('should get item label', async () => { let item = new TreeItem({ label: 'foo' }, TreeItemCollapsibleState.None) expect(getItemLabel(item)).toBe('foo') }) }) describe('show()', () => { it('should show with title', async () => { createTreeView(defaultDef) expect(treeView).toBeDefined() expect(treeView.visible).toBe(false) expect(await treeView.checkLines()).toBe(false) await treeView.show() let visible = treeView.visible expect(visible).toBe(true) await checkLines(['test', '+ a', '+ b', ' g']) treeView.registerLocalKeymap('n', undefined, () => {}) let called = false treeView.registerLocalKeymap('n', 'p', () => { called = true }, false) await helper.wait(30) await nvim.input('p') await helper.waitValue(() => called, true) }) it('should not show when visible', async () => { createTreeView(defaultDef) await treeView.show() let windowId = treeView.windowId await treeView.show() expect(treeView.windowId).toBe(windowId) }) it('should reuse window', async () => { createTreeView(defaultDef) await treeView.show() let windowId = treeView.windowId provider.dispose() createTreeView(defaultDef) await treeView.show() expect(treeView.windowId).toBe(windowId) }) it('should render item icon', async () => { createTreeView(defaultDef) nodes[0].icon = { text: 'i', hlGroup: 'Title' } nodes[1].icon = { text: 'i', hlGroup: 'Title' } nodes[2].icon = { text: 'i', hlGroup: 'Title' } await treeView.show() await checkLines(['test', '+ i a', '+ i b', ' i g']) }) }) describe('configuration', () => { it('should change open close icon', async () => { createTreeView(defaultDef) await treeView.show() let { configurations } = workspace configurations.updateMemoryConfig({ 'tree.openedIcon': '', 'tree.closedIcon': '', }) await checkLines(['test', ' a', ' b', ' g']) }) }) describe('events', () => { function waitVisibilityEvent(visible: boolean): Promise { return new Promise((resolve, reject) => { let timer = setTimeout(() => { disposable.dispose() reject(new Error('event not fired after 2s')) }, 2000) let disposable = treeView.onDidChangeVisibility(e => { clearTimeout(timer) expect(e.visible).toBe(visible) disposable.dispose() resolve(undefined) }) }) } it('should emit visibility change event', async () => { createTreeView(defaultDef) let p = waitVisibilityEvent(true) await treeView.show() await p nvim.command('close', true) await waitVisibilityEvent(false) p = waitVisibilityEvent(true) await treeView.show() await p nvim.command('enew', true) await waitVisibilityEvent(false) p = waitVisibilityEvent(true) await treeView.show() await p }) it('should dispose on tab close', async () => { await nvim.command('tabe') await nvim.command('tabe') createTreeView(defaultDef) await treeView.show() await nvim.command('close') await nvim.command('normal! 2gt') await nvim.command('close') await nvim.command('normal! 1gt') await nvim.command('tabonly') await helper.waitValue(() => { return treeView.valid }, false) }) it('should registerLocalKeymap before shown', async () => { createTreeView(defaultDef) let called = false treeView.registerLocalKeymap('n', 'p', () => { called = true }, true) await treeView.show() await events.race(['TextChanged'], 50) await nvim.input('p') await helper.waitValue(() => { return called }, true) }) }) describe('public properties', () => { it('should change title', async () => { createTreeView(defaultDef) treeView.title = 'foo' await treeView.show() await checkLines(['foo', '+ a', '+ b', ' g']) treeView.title = 'bar' await events.race(['TextChanged'], 50) await checkLines(['bar', '+ a', '+ b', ' g']) treeView.title = undefined await events.race(['TextChanged'], 50) }) it('should change description', async () => { createTreeView(defaultDef) treeView.description = 'desc' await treeView.show() await checkLines(['test desc', '+ a', '+ b', ' g']) treeView.description = 'foo bar' await events.race(['TextChanged'], 50) await checkLines(['test foo bar', '+ a', '+ b', ' g']) treeView.description = '' await events.race(['TextChanged'], 50) await checkLines(['test', '+ a', '+ b', ' g']) }) it('should change message', async () => { createTreeView(defaultDef) treeView.message = 'hello' await treeView.show() await checkLines(['hello', '', 'test', '+ a', '+ b', ' g']) treeView.message = 'foo' await events.race(['TextChanged'], 50) await checkLines(['foo', '', 'test', '+ a', '+ b', ' g']) treeView.message = undefined await events.race(['TextChanged'], 50) await checkLines(['test', '+ a', '+ b', ' g']) }) }) describe('options', () => { it('should disable winfixwidth', async () => { createTreeView(defaultDef, { winfixwidth: false }) await treeView.show() let res = await nvim.eval('&winfixwidth') expect(res).toBe(0) }) it('should disable leaf indent', async () => { createTreeView(defaultDef, { disableLeafIndent: true }) await treeView.show() await checkLines(['test', '+ a', '+ b', 'g']) }) it('should should adjust window width', async () => { let def: NodeDef[] = [ ['a', [['c'], ['d']]], ['very long line'] ] createTreeView(def, { autoWidth: true }) await treeView.show('belowright 10vs') let width = await nvim.call('winwidth', [0]) expect(width).toBeGreaterThan(10) expect(treeView.targetWinId).toBeDefined() }) it('should support many selection', async () => { createTreeView(defaultDef, { canSelectMany: true }) await treeView.show() let selection: TreeNode[] treeView.onDidChangeSelection(e => { selection = e.selection }) await nvim.command('exe 1') await nvim.input('') await helper.wait(10) await nvim.command('exe 2') await nvim.input('') await helper.waitValue(() => { return selection?.length }, 1) await nvim.command('exe 3') await nvim.input('') await helper.waitValue(() => { return selection?.length }, 2) await nvim.input('') await helper.waitValue(() => { return selection.length }, 1) let buf = await nvim.buffer let res = await nvim.call('sign_getplaced', [buf.id, { group: 'CocTree' }]) let signs = res[0].signs expect(treeView.selection.length).toBe(1) expect(signs.length).toBe(1) expect(signs[0]).toEqual({ lnum: 2, id: 3001, name: 'CocTreeSelected', priority: 10, group: 'CocTree' }) }) }) describe('key-mappings', () => { async function getSingns() { let buf = await nvim.buffer let res = await nvim.call('sign_getplaced', [buf.id, { group: 'CocTree' }]) return res[0].signs.length } it('should jump back by ', async () => { let winid = await nvim.call('win_getid') createTreeView(defaultDef) await treeView.show() await helper.wait(30) await nvim.input('') await helper.waitValue(() => { return nvim.call('win_getid', []) }, winid) }) it('should toggle selection by ', async () => { createTreeView(defaultDef) await treeView.show() let selection: TreeNode[] treeView.onDidChangeSelection(e => { selection = e.selection }) await nvim.command('exe 1') await nvim.input('') await helper.wait(10) await nvim.command('exe 2') await nvim.input('') await helper.waitValue(() => selection.length, 1) await nvim.command('exe 3') await nvim.input('') await helper.waitValue(async () => { return await getSingns() }, 1) await nvim.input('') await helper.waitValue(async () => { return await getSingns() }, 0) }) it('should reset signs after expand & collapse', async () => { createTreeView(defaultDef) await treeView.show() await nvim.command('exe 2') await nvim.input('t') await checkLines([ 'test', '- a', ' c', ' d', '+ b', ' g', ]) await nvim.command('exe 3') await nvim.input('') await helper.waitValue(() => { return getSingns() }, 1) await nvim.command('exe 2') await nvim.input('t') await helper.waitValue(() => { return getSingns() }, 0) await nvim.input('t') await helper.waitValue(() => { return getSingns() }, 1) }) it('should close tree view by close key', async () => { helper.updateConfiguration('tree.key.close', 'c') createTreeView(defaultDef) await treeView.show() await helper.wait(30) expect(treeView.visible).toBe(true) await nvim.input('c') await helper.waitValue(() => treeView.visible, false) }) it('should invoke command by ', async () => { let node: TreeNode createTreeView(defaultDef, {}, { handleClick: n => { node = n } }) await treeView.show() await treeView.invokeCommand(undefined) await nvim.input('') await helper.waitValue(() => node, undefined) await nvim.command('exe 2') await nvim.input('') await helper.waitValue(() => node && node.label, 'a') }) it('should not throw when resolve command cancelled', async () => { let node: TreeNode let cancelled = false createTreeView(defaultDef, {}, { handleClick: n => { node = n }, resolveItem: (item, _node, token) => { return new Promise(resolve => { let timer = setTimeout(() => { item.command = { title: 'not exists', command: 'test' } resolve(item) }, 5000) token.onCancellationRequested(() => { cancelled = true clearTimeout(timer) resolve(item) }) }) } }) await treeView.show() await nvim.command('exe 2') let spy = jest.spyOn(console, 'error').mockImplementation(() => { // noop }) await nvim.input('') await helper.wait(10) await nvim.command('exe 1') await helper.waitValue(() => cancelled, true) spy.mockRestore() expect(node).toBeUndefined() }) it('should toggle expand by t', async () => { createTreeView(defaultDef) let c = nodes[0].children[0] c.children = [createNode('h')] await treeView.show() await nvim.command('exe 1') await nvim.input('t') await helper.wait(10) await nvim.command('exe 3') await nvim.input('t') await helper.wait(10) await nvim.command('exe 2') await nvim.input('t') await checkLines([ 'test', '- a', ' + c', ' d', '- b', ' e', ' f', ' g' ]) await nvim.command('exe 2') await nvim.input('t') await checkLines([ 'test', '+ a', '- b', ' e', ' f', ' g' ]) }) it('should should collapse parent node by t', async () => { createTreeView(defaultDef) await treeView.show() await nvim.command('exe 2') await nvim.input('t') await checkLines([ 'test', '- a', ' c', ' d', '+ b', ' g', ]) await nvim.command('exe 3') await nvim.input('t') await checkLines([ 'test', '+ a', '+ b', ' g', ]) }) it('should collapse all nodes by M', async () => { createTreeView(defaultDef) let c = nodes[0].children[0] c.children = [createNode('h')] await treeView.show() await helper.wait(50) await nvim.command('exe 2') await nvim.input('t') await helper.wait(50) await nvim.command('exe 3') await nvim.input('t') await helper.wait(50) await nvim.command('exe 6') await nvim.input('t') await checkLines([ 'test', '- a', ' - c', ' h', ' d', '- b', ' e', ' f', ' g', ]) await nvim.input('M') await checkLines([ 'test', '+ a', '+ b', ' g', ]) let res = await treeView.checkLines() expect(res).toBe(true) }) it('should toggle expand on open/close icon click ', async () => { createTreeView(defaultDef) await treeView.show() await nvim.call('cursor', [1, 1]) await nvim.input('') await helper.wait(20) await nvim.call('cursor', [2, 1]) await nvim.input('') await checkLines([ 'test', '- a', ' c', ' d', '+ b', ' g', ]) await nvim.input('') await checkLines([ 'test', '+ a', '+ b', ' g', ]) let res = await treeView.checkLines() expect(res).toBe(true) }) it('should invoke command on node click', async () => { let node: TreeNode createTreeView(defaultDef, {}, { handleClick: n => { node = n } }) await treeView.show() await nvim.call('cursor', [2, 3]) await nvim.input('') await helper.waitValue(() => node != null, true) expect(node.label).toBe('a') }) }) describe('invokeActions', () => { it('should show warning when resolveActions does not exist', async () => { createTreeView(defaultDef) await treeView.show() await treeView.invokeActions(undefined) await nvim.call('cursor', [2, 3]) await nvim.input('') await helper.waitValue(async () => { let cmdline = await helper.getCmdline() return cmdline.includes('No actions') }, true) }) it('should show warning when resolveActions is empty', async () => { createTreeView(defaultDef, {}, { resolveActions: () => { return [] } }) await treeView.show() await nvim.call('cursor', [2, 3]) await nvim.input('') await helper.waitValue(async () => { let cmdline = await helper.getCmdline() return cmdline.includes('No actions') }, true) }) it('should invoke selected action', async () => { let args: any[] let called = false createTreeView(defaultDef, {}, { resolveActions: (item, element) => { args = [item, element] return [{ title: 'one', handler: () => { called = true } }] } }) await treeView.show() await nvim.call('cursor', [2, 3]) await nvim.input('') await helper.waitPrompt() await nvim.input('') await helper.wait(20) await nvim.input('') await helper.waitPrompt() await nvim.input('') await helper.waitValue(() => { return called }, true) expect(called).toBe(true) expect(args[0].label).toBe('a') expect(args[1].label).toBe('a') }) }) describe('events', () => { it('should emit visibility change on buffer unload', async () => { createTreeView(defaultDef) let visible treeView.onDidChangeVisibility(e => { visible = e.visible }) await treeView.show() let buf = await nvim.buffer nvim.command(`bd! ${buf.id}`, true) await helper.waitValue(() => visible, false) }) it('should show tooltip on CursorHold', async () => { createTreeView(defaultDef, {}, { resolveItem: (item, node) => { if (node.label == 'a') { item.tooltip = 'first' } if (node.label == 'b') { item.tooltip = { kind: 'markdown', value: '#title' } } return item } }) await treeView.show() await nvim.command('exe 2') let bufnr = await nvim.eval(`bufnr('%')`) as number await events.fire('CursorHold', [bufnr, [2, 1]]) let win = await helper.getFloat() expect(win).toBeDefined() let buf = await win.buffer let lines = await buf.lines expect(lines).toEqual(['first']) await nvim.command('exe 3') await events.fire('CursorHold', [bufnr, [3, 1]]) lines = await buf.lines expect(lines).toEqual(['#title']) await events.fire('CursorHold', [bufnr, [1, 1]]) }) }) describe('data change', () => { it('should ignore hidden node change', async () => { createTreeView(defaultDef) await treeView.show() let tick = await nvim.eval('b:changedtick') updateData([ ['a', [['c', [['h']]], ['d']]], ['b', [['e'], ['f']]], ['g'] ]) await helper.wait(20) let curr = await nvim.eval('b:changedtick') expect(curr).toBe(tick) }) it('should render all nodes on root change', async () => { createTreeView(defaultDef) await treeView.show() updateData([ ['g'], ['h'], ['b', [['e'], ['f']]], ['a', [['c'], ['d']]] ]) await checkLines([ 'test', ' g', ' h', '+ b', '+ a', ]) let res = await treeView.checkLines() expect(res).toBe(true) }) it('should keep node open state', async () => { createTreeView(defaultDef) let c = nodes[0].children[0] c.children = [createNode('h')] await treeView.show() await nvim.command('exe 2') await nvim.input('t') await helper.wait(50) await nvim.command('exe 3') await nvim.input('t') await helper.wait(50) await nvim.command('exe 6') await nvim.input('t') await helper.wait(50) updateData([ ['h'], ['g', [['i']]], ['b', [['f']]], ['a', [['c'], ['j']]] ]) await checkLines([ 'test', ' h', '+ g', '- b', ' f', '- a', ' c', ' j', ]) let res = await treeView.checkLines() expect(res).toBe(true) }) it('should render changed nodes', async () => { createTreeView(defaultDef) await treeView.show() await nvim.command('exe 2') await nvim.input('t') await events.race(['TextChanged']) updateData([ ['a', [['h', [['i']]], ['d']]], ['b', [['e'], ['f']]], ['g'], ]) await checkLines([ 'test', '- a', ' + h', ' d', '+ b', ' g', ]) let res = await treeView.checkLines() expect(res).toBe(true) }) it('should error message on error', async () => { createTreeView(defaultDef) await treeView.show() await nvim.command('exe 2') await nvim.input('t') await events.race(['TextChanged']) let msg = 'Unable to fetch children' provider.getChildren = () => { throw new Error(msg) } updateData([['a']]) await events.race(['TextChanged']) let line = await nvim.call('getline', [1]) expect(line).toMatch(msg) await helper.wait(50) let res = await treeView.checkLines() expect(res).toBe(true) }) it('should reset message when data exists', async () => { createTreeView([]) let curr = [] provider.getChildren = () => { return Promise.resolve(curr) } await treeView.show() await checkLines([ 'No results', '', 'test', ]) curr = [createNode('h')] await treeView.render() await checkLines([ 'test', ' h', ]) }) it('should show error message on refresh error', async () => { createTreeView(defaultDef) await treeView.show() makeUpdateUIThrowError() updateData([ ['a', [['h'], ['d']]], ['b', [['e'], ['f']]], ['g'], ]) await helper.waitValue(async () => { let line = await helper.getCmdline() return line.includes('Error on tree refresh') }, true) }) it('should render deprecated node with deprecated highlight', async () => { createTreeView(defaultDef) await treeView.show() let defs: NodeDef[] = [ ['a'], ['b'] ] let nodes = createNodes(defs) nodes[0].deprecated = true provider.update(nodes) await checkLines([ 'test', ' a', ' b', ]) let ns = await nvim.call('coc#highlight#create_namespace', ['tree']) let bufnr = await nvim.call('bufnr', ['%']) let markers = await nvim.call('nvim_buf_get_extmarks', [bufnr, ns, [1, 0], [1, -1], { details: true }]) as any[] expect(markers.length > 0).toBe(true) expect(markers[0][3]['hl_group']).toBe('CocDeprecatedHighlight') }) it('should not throw when getTreeItem return undefined', async () => { let provider: TreeDataProvider = { getTreeItem: (): TreeItem => { return undefined }, getChildren: (): ProviderResult => { return [{ label: 'a' }] } } let treeView = new TreeView('test', { bufhidden: 'hide', treeDataProvider: provider }) await treeView.show() await checkLines([ 'test', ]) treeView.dispose() }) }) describe('focusItem()', () => { it('should not throw when node not rendered', async () => { createTreeView(defaultDef) treeView.selectItem(undefined) treeView.focusItem(nodes[0]) treeView.unselectItem(999) await treeView.show() let c = nodes[0].children[0] await treeView.onHover(3) treeView.focusItem(c) treeView.focusItem(undefined) }) it('should focus rendered node', async () => { createTreeView(defaultDef) await treeView.show() treeView.focusItem(nodes[1]) let line = await nvim.call('getline', ['.']) expect(line).toBe('+ b') }) }) describe('reveal()', () => { it('should throw error when getParent does not exist', async () => { createTreeView(defaultDef) provider.getParent = undefined await treeView.show() let err try { await treeView.reveal(nodes[0].children[0]) } catch (e) { err = e } expect(err).toBeDefined() }) it('should select item', async () => { createTreeView(defaultDef) let c = nodes[0].children[0] let h = createNode('h') c.children = [h] await treeView.show() await treeView.reveal(h, { expand: true }) await checkLines([ 'test', '- a', ' - c', ' h', ' d', '+ b', ' g', ]) let selection = treeView.selection expect(selection.length).toBe(1) expect(selection[0].label).toBe('h') let line = await nvim.call('getline', ['.']) expect(line).toMatch('h') }) it('should not select item', async () => { createTreeView(defaultDef) await treeView.show() await treeView.reveal(nodes[1], { select: false }) let lnum = await nvim.call('line', ['.']) expect(lnum).toBe(1) }) it('should focus item', async () => { createTreeView(defaultDef) await treeView.show() await treeView.reveal(nodes[1], { focus: true }) let line = await nvim.call('getline', ['.']) expect(line).toMatch('b') }) it('should expand item which single level', async () => { createTreeView(defaultDef) let c = nodes[0].children[0] c.children = [createNode('h')] await treeView.show() await treeView.reveal(nodes[0], { expand: true }) await checkLines([ 'test', '- a', ' + c', ' d', '+ b', ' g', ]) }) it('should expand item which 2 level', async () => { createTreeView(defaultDef) let c = nodes[0].children[0] c.children = [createNode('h')] await treeView.show() await treeView.reveal(nodes[0], { expand: 2 }) await checkLines([ 'test', '- a', ' - c', ' h', ' d', '+ b', ' g', ]) }) }) describe('filter', () => { afterEach(() => { nvim.call('coc#prompt#stop_prompt', ['filter'], true) }) async function createFilterTreeView(opts: Partial> = {}): Promise { createTreeView(defaultDef, { enableFilter: true }, opts) await treeView.show() await helper.wait(20) let tick = await nvim.eval('b:changedtick') as number await nvim.input('f') await helper.waitValue(async () => { let c = await nvim.eval('b:changedtick') as number return c - tick > 1 }, true) } it('should start filter by input', async () => { await createFilterTreeView() await treeView.reveal(undefined) await checkLines([ 'test', ' ', ' a', ' c', ' d', ' b', ' e', ' f', ' g' ]) await nvim.input('a') await helper.waitFor('getline', [2], 'a ') }) it('should not throw error on filter', async () => { await createFilterTreeView() let spy = jest.spyOn(treeView as any, 'getRenderedLine').mockImplementation(() => { throw new Error('Error on updateUI') }) await nvim.input('a') await helper.wait(50) spy.mockRestore() }) it('should add & remove Cursor highlight on window change', async () => { let winid = await nvim.call('win_getid') let ns = await nvim.call('coc#highlight#create_namespace', ['tree']) await createFilterTreeView() let bufnr = await nvim.call('bufnr', ['%']) let markers = await nvim.call('nvim_buf_get_extmarks', [bufnr, ns, [1, 0], [1, -1], {}]) as [number, number, number][] expect(markers[0]).toBeDefined() await nvim.call('win_gotoid', [winid]) markers = await nvim.call('nvim_buf_get_extmarks', [bufnr, ns, [1, 0], [1, -1], {}]) as [number, number, number][] expect(markers.length).toBe(0) await nvim.command('wincmd p') markers = await nvim.call('nvim_buf_get_extmarks', [bufnr, ns, [1, 0], [1, -1], {}]) as [number, number, number][] expect(markers.length).toBe(1) }) it('should filter new nodes on data change', async () => { await createFilterTreeView() await nvim.input('a') await helper.wait(50) updateData([ ['ab'], ['e'], ['fA'] ]) await helper.waitValue(async () => { return await nvim.call('getline', [1, '$']) }, ['test', 'a ', ' ab', ' fA',]) }) it('should change selected item by and ', async () => { await createFilterTreeView() await nvim.input('a') await helper.wait(50) updateData([ ['ab'], ['fA'] ]) await helper.wait(30) await nvim.input('') await helper.waitValue(() => { let curr = treeView.selection[0] return curr.label }, 'fA') await nvim.input('') await helper.waitValue(() => { let curr = treeView.selection[0] return curr.label }, 'ab') await nvim.input('') await helper.waitValue(() => { let curr = treeView.selection[0] return curr.label }, 'fA') await nvim.input('') await helper.waitValue(() => { let curr = treeView.selection[0] return curr.label }, 'ab') }) it('should not throw with empty nodes', async () => { await createFilterTreeView() await nvim.input('ab') await helper.wait(50) await nvim.input('') await helper.wait(50) await nvim.input('') await helper.wait(50) await nvim.input('') await checkLines(['test', 'ab ']) let curr = treeView.selection[0] expect(curr).toBeUndefined() }) it('should invoke command by ', async () => { let node await createFilterTreeView({ handleClick: n => { node = n } }) await nvim.input('') await helper.waitValue(() => node != null, true) let curr = treeView.selection[0] expect(curr).toBeDefined() }) it('should keep state when press with empty selection', async () => { await createFilterTreeView() await nvim.input('ab') await helper.wait(50) await nvim.input('') await checkLines(['test', 'ab ']) }) it('should delete last filter character by ', async () => { await createFilterTreeView() await nvim.input('a') await helper.wait(20) await nvim.input('') await checkLines([ 'test', ' ', ' a', ' c', ' d', ' b', ' e', ' f', ' g' ]) }) it('should clean filter character by ', async () => { await createFilterTreeView() await nvim.input('ab') await helper.wait(20) await nvim.input('') await checkLines([ 'test', ' ', ' a', ' c', ' d', ' b', ' e', ' f', ' g' ]) }) it('should cancel filter by and ', async () => { await createFilterTreeView() await helper.waitPrompt() await nvim.input('') await checkLines([ 'test', '+ a', '+ b', ' g', ]) await nvim.input('f') await helper.wait(20) await nvim.input('') await checkLines([ 'test', '+ a', '+ b', ' g', ]) }) it('should navigate input history by and ', async () => { await createFilterTreeView() await nvim.input('a') await helper.wait(10) await nvim.input('') await helper.wait(10) await nvim.input('f') await helper.wait(10) await nvim.input('b') await helper.wait(10) await nvim.input('') await helper.wait(10) await nvim.input('f') await helper.wait(10) await nvim.input('') await checkLines(['test', 'b ', ' b',]) await nvim.input('') await checkLines(['test', 'a ', ' a',]) }) it('should not throw on filter error', async () => { await createFilterTreeView() let spy = jest.spyOn(treeView as any, 'redraw').mockImplementation(() => { throw new Error('test error') }) await nvim.input('a') await helper.wait(20) spy.mockRestore() }) }) }) ================================================ FILE: src/__tests__/ultisnips.py ================================================ __requesting = True def coc_UltiSnips_create(): import re, vim, os, sys from collections import defaultdict, namedtuple _Placeholder = namedtuple("_Placeholder", ["current_text", "start", "end"]) _VisualContent = namedtuple("_VisualContent", ["mode", "text"]) _Position = namedtuple("_Position", ["line", "col"]) # is_vim = vim.eval('has("nvim")') == '0' def byte2col(line, nbyte): """Convert a column into a byteidx suitable for a mark or cursor position inside of vim.""" line = vim.current.buffer[line - 1] raw_bytes = line.encode(vim.eval("&encoding"), "replace")[:nbyte] return len(raw_bytes.decode(vim.eval("&encoding"), "replace")) def col2byte(line, col): """Convert a valid column index into a byte index inside of vims buffer.""" # We pad the line so that selecting the +1 st column still works. pre_chars = (vim.current.buffer[line - 1] + " ")[:col] return len(pre_chars.encode(vim.eval("&encoding"), "replace")) def get_visual_content(): mode = vim.eval("visualmode()") if mode == '': return '' sl, sbyte = map( int, (vim.eval("""line("'<")"""), vim.eval("""col("'<")""")) ) el, ebyte = map( int, (vim.eval("""line("'>")"""), vim.eval("""col("'>")""")) ) sc = byte2col(sl, sbyte - 1) ec = byte2col(el, ebyte - 1) # When 'selection' is 'exclusive', the > mark is one column behind the # actual content being copied, but never before the < mark. if vim.eval("&selection") == "exclusive": if not (sl == el and sbyte == ebyte): ec -= 1 _vim_line_with_eol = lambda ln: vim.current.buffer[ln] + "\n" if sl == el: text = _vim_line_with_eol(sl - 1)[sc : ec + 1] else: text = _vim_line_with_eol(sl - 1)[sc:] for cl in range(sl, el - 1): text += _vim_line_with_eol(cl) text += _vim_line_with_eol(el - 1)[: ec + 1] return text def _expand_anon(value, trigger=""): pos = vim.eval('coc#cursor#position()') line = int(pos[0]) character = int(pos[1]) args = r'[{"start":{"line":%d,"character":%d},"end":{"line":%d,"character":%d}}, "%s", v:null, {}]' % (line, character - len(trigger), line, character, re.sub(r'"', r'\\"', value.replace('\\', '\\\\'))) # Python of vim doesn't have event loop, use vim's timer if __requesting: vim.eval(r'coc#util#timer("coc#rpc#notify",["snippetInsert", %s])' % (args)) else: code = r'coc#rpc#request("snippetInsert", %s)' % (args) vim.eval(code) vim.command('redraw') def expand_anon(value, trigger="", cursor = None): if len(value) == 0: return _expand_anon(value, trigger) if cursor is not None: cursor.preserve() class Position: """Represents a Position in a text file: (0 based line index, 0 based column index) and provides methods for moving them around.""" def __init__(self, line, col): self.line = line self.col = col def move(self, pivot, delta): """'pivot' is the position of the first changed character, 'delta' is how text after it moved.""" if self < pivot: return if delta.line == 0: if self.line == pivot.line: self.col += delta.col elif delta.line > 0: if self.line == pivot.line: self.col += delta.col - pivot.col self.line += delta.line else: self.line += delta.line if self.line == pivot.line: self.col += -delta.col + pivot.col def __add__(self, pos): assert isinstance(pos, Position) return Position(self.line + pos.line, self.col + pos.col) def __sub__(self, pos): assert isinstance(pos, Position) return Position(self.line - pos.line, self.col - pos.col) def __eq__(self, other): return (self.line, self.col) == (other.line, other.col) def __ne__(self, other): return (self.line, self.col) != (other.line, other.col) def __lt__(self, other): return (self.line, self.col) < (other.line, other.col) def __le__(self, other): return (self.line, self.col) <= (other.line, other.col) def __repr__(self): return "(%i,%i)" % (self.line, self.col) def __getitem__(self, index): if index > 1: raise IndexError("position can be indexed only 0 (line) and 1 (column)") if index == 0: return self.line else: return self.col def diff(a, b, sline=0): """ Return a list of deletions and insertions that will turn 'a' into 'b'. This is done by traversing an implicit edit graph and searching for the shortest route. The basic idea is as follows: - Matching a character is free as long as there was no deletion/insertion before. Then, matching will be seen as delete + insert [1]. - Deleting one character has the same cost everywhere. Each additional character costs only have of the first deletion. - Insertion is cheaper the earlier it happens. The first character is more expensive that any later [2]. [1] This is that world -> aolsa will be "D" world + "I" aolsa instead of "D" w , "D" rld, "I" a, "I" lsa [2] This is that "hello\n\n" -> "hello\n\n\n" will insert a newline after hello and not after \n """ d = defaultdict(list) # pylint:disable=invalid-name seen = defaultdict(lambda: sys.maxsize) d[0] = [(0, 0, sline, 0, ())] cost = 0 deletion_cost = len(a) + len(b) insertion_cost = len(a) + len(b) while True: while len(d[cost]): x, y, line, col, what = d[cost].pop() if a[x:] == b[y:]: return what if x < len(a) and y < len(b) and a[x] == b[y]: ncol = col + 1 nline = line if a[x] == "\n": ncol = 0 nline += 1 lcost = cost + 1 if ( what and what[-1][0] == "D" and what[-1][1] == line and what[-1][2] == col and a[x] != "\n" ): # Matching directly after a deletion should be as costly as # DELETE + INSERT + a bit lcost = (deletion_cost + insertion_cost) * 1.5 if seen[x + 1, y + 1] > lcost: d[lcost].append((x + 1, y + 1, nline, ncol, what)) seen[x + 1, y + 1] = lcost if y < len(b): # INSERT ncol = col + 1 nline = line if b[y] == "\n": ncol = 0 nline += 1 if ( what and what[-1][0] == "I" and what[-1][1] == nline and what[-1][2] + len(what[-1][-1]) == col and b[y] != "\n" and seen[x, y + 1] > cost + (insertion_cost + ncol) // 2 ): seen[x, y + 1] = cost + (insertion_cost + ncol) // 2 d[cost + (insertion_cost + ncol) // 2].append( ( x, y + 1, line, ncol, what[:-1] + (("I", what[-1][1], what[-1][2], what[-1][-1] + b[y]),), ) ) elif seen[x, y + 1] > cost + insertion_cost + ncol: seen[x, y + 1] = cost + insertion_cost + ncol d[cost + ncol + insertion_cost].append( (x, y + 1, nline, ncol, what + (("I", line, col, b[y]),)) ) if x < len(a): # DELETE if ( what and what[-1][0] == "D" and what[-1][1] == line and what[-1][2] == col and a[x] != "\n" and what[-1][-1] != "\n" and seen[x + 1, y] > cost + deletion_cost // 2 ): seen[x + 1, y] = cost + deletion_cost // 2 d[cost + deletion_cost // 2].append( ( x + 1, y, line, col, what[:-1] + (("D", line, col, what[-1][-1] + a[x]),), ) ) elif seen[x + 1, y] > cost + deletion_cost: seen[x + 1, y] = cost + deletion_cost d[cost + deletion_cost].append( (x + 1, y, line, col, what + (("D", line, col, a[x]),)) ) cost += 1 class VimBuffer: """Wrapper around the current Vim buffer.""" def __getitem__(self, idx): return vim.current.buffer[idx] def __setitem__(self, idx, text): vim.current.buffer[idx] = text def __len__(self): return len(vim.current.buffer) @property def number(self): # pylint:disable=no-self-use """The bufnr() of the current buffer.""" return vim.current.buffer.number @property def filetypes(self): return [ft for ft in vim.eval("&filetype").split(".") if ft] class VimBufferProxy(VimBuffer): """ Proxy object used for tracking changes that made from snippet actions. Unfortunately, vim by itself lacks of the API for changing text in trackable maner. Vim marks offers limited functionality for tracking line additions and deletions, but nothing offered for tracking changes within single line. Instance of this class is passed to all snippet actions and behaves as internal vim.current.window.buffer. All changes that are made by user passed to diff algorithm, and resulting diff applied to internal snippet structures to ensure they are in sync with actual buffer contents. """ def __init__(self, handlers): """ Instantiate new object. """ self._buffer = vim.current.buffer self._change_tick = int(vim.eval("b:changedtick")) self._forward_edits = True self._handlers = handlers def is_buffer_changed_outside(self): """ Returns true, if buffer was changed without using proxy object, like with vim.command() or through internal vim.current.window.buffer. """ return self._change_tick < int(vim.eval("b:changedtick")) def validate_buffer(self): """ Raises exception if buffer is changed beyond proxy object. """ if self.is_buffer_changed_outside(): raise os.error( "buffer was modified using vim.command or " + "vim.current.buffer; that changes are untrackable and leads to " + "errors in snippet expansion; use special variable `snip.buffer` " "for buffer modifications.\n\n" + "See :help UltiSnips-buffer-proxy for more info." ) def __setitem__(self, key, value): """ Behaves as vim.current.window.buffer.__setitem__ except it tracks changes and applies them to the current snippet stack. """ if isinstance(key, slice): value = [line for line in value] changes = list(self._get_diff(key.start, key.stop, value)) self._buffer[key.start : key.stop] = [line.strip("\n") for line in value] else: value = value changes = list(self._get_line_diff(key, self._buffer[key], value)) self._buffer[key] = value self._change_tick += 1 if self._forward_edits: for change in changes: self._apply_change(change) def __setslice__(self, i, j, text): """ Same as __setitem__. """ self.__setitem__(slice(i, j), text) def __getitem__(self, key): """ Just passing call to the vim.current.window.buffer.__getitem__. """ return self._buffer[key] def __getslice__(self, i, j): """ Same as __getitem__. """ return self.__getitem__(slice(i, j)) def __len__(self): """ Same as len(vim.current.window.buffer). """ return len(self._buffer) def append(self, line, line_number=-1): """ Same as vim.current.window.buffer.append(), but with tracking changes. """ if line_number < 0: line_number = len(self) if not isinstance(line, list): line = [line] self[line_number:line_number] = [l for l in line] def insert(self, index, line): self[index:index] = [line] def __delitem__(self, key): if isinstance(key, slice): self.__setitem__(key, []) else: self.__setitem__(slice(key, key + 1), []) def _get_diff(self, start, end, new_value): """ Very fast diffing algorithm when changes are across many lines. """ for line_number in range(start, end): if line_number < 0: line_number = len(self._buffer) + line_number yield ("D", line_number, 0, self._buffer[line_number], True) if start < 0: start = len(self._buffer) + start for line_number in range(0, len(new_value)): yield ("I", start + line_number, 0, new_value[line_number], True) def _get_line_diff(self, line_number, before, after): """ Use precise diffing for tracking changes in single line. """ if before == "": for change in self._get_diff(line_number, line_number + 1, [after]): yield change else: for change in diff(before, after): yield (change[0], line_number, change[2], change[3]) def _apply_change(self, change): """ Apply changeset to current snippets stack, correctly moving around snippet itself or its child. """ # ('I', 4, 0, 'xy') # change_type, line_number, column_number, change_text = change[0:4] if len(self._handlers) > 0: for handler in self._handlers: handler._apply_change(change) else: print(str(change)) pass def _disable_edits(self): """ Temporary disable applying changes to snippets stack. Should be done while expanding anonymous snippet in the middle of jump to prevent double tracking. """ self._forward_edits = False def _enable_edits(self): """ Enables changes forwarding back. """ self._forward_edits = True class PositionWrapper(object): def __init__(self, position): self._position = position self._valid = True def _apply_change(self, change): # ('I', 4, 0, 'xy', True) pos = self._position start = Position(change[1], change[2]) col = change[2] insert = change[0] == 'I' newline = len(change) > 4 if newline: if not insert and change[1] == pos.line: self._valid = False return else: if change[1] == pos.line: if not insert and change[2] + len(change[3]) > pos.col: self._valid = False return col = len(change[3]) if insert else 0 - len(change[3]) lc = 0 if newline: lc = 1 if insert else -1 pos.move(start, _Position(lc, col)) @property def valid(self): return self._valid @property def position(self): return self._position class SnippetUtilCursor(object): def __init__(self, cursor): self._cursor = [cursor[0] - 1, cursor[1]] self._set = False def preserve(self): self._set = True cursor = vim.current.window.cursor self._cursor = [cursor[0] - 1, cursor[1]] def is_set(self): return self._set def set(self, line, column): self.__setitem__(0, line) self.__setitem__(1, column) # vim.current.window.cursor = self.to_vim_cursor() def to_vim_cursor(self): return (self._cursor[0] + 1, self._cursor[1]) def __getitem__(self, index): return self._cursor[index] def __setitem__(self, index, value): self._set = True self._cursor[index] = value def __len__(self): return 2 def __str__(self): return str((self._cursor[0], self._cursor[1])) class IndentUtil(object): """Utility class for dealing properly with indentation.""" def __init__(self): self.reset() def reset(self): """Gets the spacing properties from Vim.""" self.shiftwidth = int( vim.eval("exists('*shiftwidth') ? shiftwidth() : &shiftwidth") ) self._expandtab = vim.eval("&expandtab") == "1" self._tabstop = int(vim.eval("&tabstop")) def ntabs_to_proper_indent(self, ntabs): """Convert 'ntabs' number of tabs to the proper indent prefix.""" line_ind = ntabs * self.shiftwidth * " " line_ind = self.indent_to_spaces(line_ind) line_ind = self.spaces_to_indent(line_ind) return line_ind def indent_to_spaces(self, indent): """Converts indentation to spaces respecting Vim settings.""" indent = indent.expandtabs(self._tabstop) right = (len(indent) - len(indent.rstrip(" "))) * " " indent = indent.replace(" ", "") indent = indent.replace("\t", " " * self._tabstop) return indent + right def spaces_to_indent(self, indent): """Converts spaces to proper indentation respecting Vim settings.""" if not self._expandtab: indent = indent.replace(" " * self._tabstop, "\t") return indent class BaseContext(object): def __init__(self): super().__init__() self._cursor = SnippetUtilCursor(vim.current.window.cursor) line = self._cursor[0] pos = Position(line, byte2col(line + 1, self._cursor[1])) wrapper = PositionWrapper(pos) self._handlers = [wrapper] self._buffer = VimBufferProxy(self._handlers) @property def window(self): return vim.current.window @property def buffer(self): return self._buffer @property def cursor(self): return self._cursor @property def line(self): return vim.current.window.cursor[0] - 1 @property def column(self): return vim.current.window.cursor[1] @property def visual_mode(self): return vim.eval("visualmode()") @property def visual_text(self): if "coc_selected_text" in vim.vars: return vim.vars["coc_selected_text"] return '' @property def last_placeholder(self): if "coc_last_placeholder" in vim.vars: p = vim.vars["coc_last_placeholder"] start = _Position(p["start"]["line"], p["start"]["col"]) end = _Position(p["end"]["line"], p["end"]["col"]) return _Placeholder(p["current_text"], start, end) return None def expand_anon(self, value, trigger="", description="", options="", context=None, actions=None): expand_anon(value, trigger, self._cursor) return True class SnippetUtil(object): def __init__(self, _initial_indent, start, end, context): self._ind = IndentUtil() self._visual = _VisualContent( vim.eval("visualmode()"), vim.eval('get(g:,"coc_selected_text","")') ) self._initial_indent = _initial_indent self._reset("") self._start = Position(start[0], start[1]) self._end = Position(end[0], end[1]) self._context = context def _reset(self, cur): """Gets the snippet ready for another update. :cur: the new value for c. """ self._ind.reset() self._cur = cur self._rv = "" self._changed = False self.reset_indent() def shift(self, amount=1): """Shifts the indentation level. Note that this uses the shiftwidth because thats what code formatters use. :amount: the amount by which to shift. """ self.indent += " " * self._ind.shiftwidth * amount def unshift(self, amount=1): """Unshift the indentation level. Note that this uses the shiftwidth because thats what code formatters use. :amount: the amount by which to unshift. """ by = -self._ind.shiftwidth * amount try: self.indent = self.indent[:by] except IndexError: self.indent = "" def mkline(self, line="", indent=None): """Creates a properly set up line. :line: the text to add :indent: the indentation to have at the beginning if None, it uses the default amount """ if indent is None: indent = self.indent # this deals with the fact that the first line is # already properly indented if "\n" not in self._rv: try: indent = indent[len(self._initial_indent) :] except IndexError: indent = "" indent = self._ind.spaces_to_indent(indent) return indent + line def reset_indent(self): """Clears the indentation.""" self.indent = self._initial_indent def expand_anon(self, value, trigger="", description="", options="", context=None, actions=None): expand_anon(value, trigger) return True # Utility methods @property def fn(self): # pylint:disable=no-self-use,invalid-name """The filename.""" return vim.eval('expand("%:t")') or "" @property def basename(self): # pylint:disable=no-self-use """The filename without extension.""" return vim.eval('expand("%:t:r")') or "" @property def ft(self): # pylint:disable=invalid-name """The filetype.""" return self.opt("&filetype", "") @property def rv(self): # pylint:disable=invalid-name """The return value. The text to insert at the location of the placeholder. """ return self._rv @rv.setter def rv(self, value): # pylint:disable=invalid-name """See getter.""" self._changed = True self._rv = value @property def _rv_changed(self): """True if rv has changed.""" return self._changed @property def c(self): # pylint:disable=invalid-name """The current text of the placeholder.""" return self._cur @property def v(self): # pylint:disable=invalid-name """Content of visual expansions.""" return self._visual @property def p(self): if "coc_last_placeholder" in vim.vars: p = vim.vars["coc_last_placeholder"] start = _Position(p["start"]["line"], p["start"]["col"]) end = _Position(p["end"]["line"], p["end"]["col"]) return _Placeholder(p["current_text"], start, end) return None @property def context(self): return self._context def opt(self, option, default=None): # pylint:disable=no-self-use """Gets a Vim variable.""" if vim.eval("exists('%s')" % option) == "1": try: return vim.eval(option) except vim.error: pass return default def __add__(self, value): """Appends the given line to rv using mkline.""" self.rv += "\n" # pylint:disable=invalid-name self.rv += self.mkline(value) return self def __lshift__(self, other): """Same as unshift.""" self.unshift(other) def __rshift__(self, other): """Same as shift.""" self.shift(other) @property def snippet_start(self): """ Returns start of the snippet in format (line, column). """ return self._start @property def snippet_end(self): """ Returns end of the snippet in format (line, column). """ return self._end @property def buffer(self): return vim.current.buffer class ContextSnippet(BaseContext): def __init__(self): super().__init__() self._before = vim.eval('strpart(getline("."), 0, col(".") - 1)') self._after = vim.eval('strpart(getline("."), col(".") - 1)') @property def before(self): return self._before @property def after(self): return self._after class PreExpandContext(BaseContext): @property def visual_content(self): # pylint:disable=no-self-use if "coc_selected_text" in vim.vars: return vim.vars["coc_selected_text"] return '' def getResult(self): wrapper = self._handlers[0] valid = wrapper.valid and not self._cursor.is_set() if (self._cursor.is_set()): vimcursor = self._cursor.to_vim_cursor() else: if wrapper.valid: position = wrapper.position line = position.line + 1 vimcursor = (line, col2byte(line, position.col)) else: vimcursor = vim.current.window.cursor # vim.current.window.cursor = vimcursor # 0 based, line - character cursor = [vimcursor[0] - 1, byte2col(vimcursor[0], vimcursor[1])] return [valid, cursor] class PostExpandContext(BaseContext): def __init__(self, positions): super().__init__() self._start = PositionWrapper(Position(positions[0], positions[1])) self._end = PositionWrapper(Position(positions[2], positions[3])) self._handlers.extend([self._start, self._end]) @property def snippet_start(self): return self._start.position @property def snippet_end(self): return self._end.position class PostJumpContext(PostExpandContext): def __init__(self, positions, tabstop, forward): super().__init__(positions) self.tabstop = tabstop self.jump_direction = 1 if forward else -1 @property def tabstops(self): vimtabstops = vim.vars["coc_ultisnips_tabstops"] if vimtabstops is None: return {} tabstops = {} for stop in vimtabstops: index = stop['index'] indexes = stop['range'] start = _Position(indexes[0], indexes[1]) end = _Position(indexes[2], indexes[3]) tabstops[index] = _Placeholder(stop['text'], start, end) return tabstops namespace = { 'SnippetUtil': SnippetUtil, 'ContextSnippet': ContextSnippet, 'PreExpandContext': PreExpandContext, 'PostExpandContext': PostExpandContext, 'PostJumpContext': PostJumpContext, } return namespace coc_ultisnips_dict = coc_UltiSnips_create() SnippetUtil = coc_ultisnips_dict['SnippetUtil'] ContextSnippet = coc_ultisnips_dict['ContextSnippet'] # vim:set et sw=4 ts=4: ================================================ FILE: src/__tests__/vim.test.ts ================================================ process.env.VIM_NODE_RPC = '1' import type { Buffer, Neovim, Tabpage, Window } from '@chemzqm/neovim' import fs from 'fs' import os from 'os' import path from 'path' import util from 'util' import { v4 as uuid } from 'uuid' import { Position, Range, TextEdit, type Disposable } from 'vscode-languageserver-protocol' import type { CompleteResult, ExtendedCompleteItem } from '../completion/types' import events from '../events' import type { VirtualTextItem } from '../handler/inlayHint/buffer' import { sameFile } from '../util/fs' import { type Helper } from './helper' // make sure VIM_NODE_RPC take effect first const helper = require('./helper').default as Helper function disposeAll(disposables: Disposable[]): void { while (disposables.length) { const item = disposables.pop() item?.dispose() } } const disposables: Disposable[] = [] let nvim: Neovim let featuredPropList = false beforeAll(async () => { await helper.setupVim() nvim = helper.workspace.nvim // for text_padding_left of property if (helper.workspace.has('patch-9.0.1782')) { featuredPropList = true } }) afterEach(() => { disposeAll(disposables) }) afterAll(async () => { await helper.shutdown() }) async function createTmpFile(content: string, disposables?: Disposable[]): Promise { let tmpFolder = path.join(os.tmpdir(), `coc-${process.pid}`) if (!fs.existsSync(tmpFolder)) { fs.mkdirSync(tmpFolder) } let fsPath = path.join(tmpFolder, uuid()) await util.promisify(fs.writeFile)(fsPath, content, 'utf8') if (disposables) { disposables.push({ dispose: () => { if (fs.existsSync(fsPath)) fs.unlinkSync(fsPath) } }) } return fsPath } describe('workspace', () => { it('should not has nvim feature', () => { expect(helper.workspace.has('nvim-0.4.0')).toBe(false) expect(helper.workspace.has('patch-9.0.0000')).toBe(true) }) }) describe('vim api', () => { it('should start server', async () => { await nvim.setLine('foobar') let buf = await nvim.buffer let lines = await buf.lines expect(lines).toEqual(['foobar']) await nvim.command('bd!') }) it('should show info', async () => { global.REVISION = '2e82259f' let handler = helper.plugin.getHandler().workspace await handler.showInfo() await nvim.command('bd!') }) it('should navigate complete items', async () => { helper.updateConfiguration('suggest.noselect', true) const sources = require('../completion/sources').default let name = Math.random().toString(16).slice(-6) let disposable = sources.createSource({ name, doComplete: (_opt): Promise> => new Promise(resolve => { resolve({ items: [{ word: 'foo\nbar' }, { word: 'word' }] }) }) }) await nvim.input('i') nvim.call('coc#start', { source: name }, true) await helper.waitPopup() await nvim.call('coc#pum#_navigate', [1, 1]) await helper.waitFor('getline', ['.'], 'foo') expect(helper.completion.isActivated).toBe(true) await nvim.call('coc#pum#close', ['cancel']) await nvim.input('') await helper.waitFor('mode', [], 'n') disposable.dispose() await nvim.command('silent! %bwipeout!') }) it('should echo message by callTimer', async () => { const ui = require('../core/ui') ui.echoMessages(nvim, 'message', 'more', 'more') await helper.waitValue(async () => { let line = await helper.getCmdline() return line.includes('message') }, true) }) it('should call async', async () => { const funcs = require('../core/funcs') await nvim.command('normal! gg') let res = await funcs.callAsync(nvim, 'line', ['.']) expect(res).toBe(1) }) }) describe('call_function', () => { beforeAll(async () => { let folder = path.resolve(__dirname) await nvim.command(`set runtimepath+=${folder}`) }) it('should throw when call vim9 void function', async () => { await expect(async () => { await nvim.call('vim9#Execute', ['g:x = $"foo"']) }).rejects.toThrow(Error) // should not report error nvim.call('vim9#Execute', ['g:x = $"abc"'], true) let x = await nvim.getVar('x') expect(x).toBe('abc') }) it('should call dict function', async () => { let res = await nvim.callDictFunction({ key: 1 }, 'legacy#dict_add') expect(res).toBe(2) }) it('should use notify for execute', async () => { nvim.call('execute', 'let g:x = "a"."b"', true) let res = await nvim.getVar('x') expect(res).toBe('ab') }) it('should not throw for win_execute', async () => { // old style syntax await nvim.call('execute', ['let g:y = "a"."b"']) let y = await nvim.getVar('y') expect(y).toBe('ab') // new style syntax in vim9 function let res = await nvim.call('vim9#WinExecute', []) expect(res).toBe(true) // old style syntax win_execute in legacy function await nvim.call('legacy#win_execute', []) let win = await nvim.window let val = await win.getVar('foo') expect(val).toBe('ab') }) it('should eval with legacy syntax', async () => { let res = await nvim.call('eval', ['"a"."b"']) expect(res).toBe('ab') }) it('should not conflict with global function', async () => { await nvim.exec([ 'function! Win_execute(...) abort', ' throw "my error"', 'endfunction' ].join('\n')) let winid = await nvim.call('win_getid') as number await nvim.call('win_execute', [winid, 'let w:f = "b"']) let win = nvim.createWindow(winid) let val = await win.getVar('f') expect(val).toBe('b') }) }) describe('client API', () => { it('should set current dir', async () => { await nvim.setDirectory(__dirname) let res = await nvim.call('getcwd') as string expect(sameFile(res, __dirname)).toBe(true) }) it('should input characters', async () => { await nvim.input('iabc') await helper.waitFor('getline', ['.'], 'abc') await nvim.input('') await helper.waitFor('mode', [], 'n') await nvim.command('bwipeout!') }) it('should set var', async () => { await nvim.setVar('foo', 'bar', false) let res = await nvim.getVar('foo') expect(res).toBe('bar') }) it('should del var', async () => { await expect(async () => { nvim.pauseNotification() nvim.deleteVar('not_exists') await nvim.resumeNotification() }).rejects.toThrow(Error) await nvim.setVar('foo', 'bar', false) nvim.deleteVar('foo') let res = await nvim.getVar('foo') expect(res).toBeNull() }) it('should set option', async () => { await nvim.setOption('emoji', false) let res = await nvim.getOption('emoji') expect(res).toBe(false) }) it('should set current buffer', async () => { let bufnr = await nvim.call('bufadd', ['foo']) as number await nvim.call('bufload', [bufnr]) await nvim.setBuffer(nvim.createBuffer(bufnr)) let b = await nvim.buffer expect(b.id).toBe(bufnr) await nvim.command('silent! %bwipeout!') }) it('should execute vim script', async () => { let output = await nvim.exec(`echo 'foo'\necho 'bar'`, true) expect(output).toBe('foo\nbar') output = await nvim.exec(`let g:x = '5'\nunlet g:x`) expect(output).toBe('') }) it('should create new buffer', async () => { let buf = await nvim.createNewBuffer() let valid = await buf.valid expect(valid).toBe(true) let listed = await buf.getOption('buflisted') expect(listed).toBe(false) buf = await nvim.createNewBuffer(true, true) valid = await buf.valid expect(valid).toBe(true) listed = await buf.getOption('buflisted') expect(listed).toBe(true) let buftype = await buf.getOption('buftype') expect(buftype).toBe('nofile') }) it('should set current window', async () => { let winid = await nvim.call('win_getid') as number await nvim.command('sp | sp | sp') let win = nvim.createWindow(winid) await nvim.setWindow(win) let curr = await nvim.call('win_getid') as number expect(curr).toBe(winid) await nvim.command('only!') }) it('should set current tabpage', async () => { let tab = await nvim.tabpage await nvim.command('tabe') await nvim.setTabpage(tab) let nr = await nvim.call('tabpagenr') expect(nr).toBe(tab.id) let tabpages = await nvim.tabpages expect(tabpages.length).toBe(2) await nvim.command('tabonly!') }) it('should list windows', async () => { let wins = await nvim.windows expect(Array.isArray(wins)).toBe(true) }) it('should call atomic', async () => { await expect(async () => { nvim.pauseNotification() nvim.call('abc', [], true) await nvim.resumeNotification() }).rejects.toThrow(Error) let res = await nvim.getVvar('errmsg') expect(res).toBe('') }) it('should execute command', async () => { await nvim.command('sp') let wins = await nvim.windows expect(wins.length).toBe(2) await nvim.command('only') wins = await nvim.windows expect(wins.length).toBe(1) }) it('should allow legacy script on command', async () => { await nvim.command('let g:x = v:argv[0]." bar"') let res = await nvim.getVar('x') expect(res).toMatch('bar') }) it('should not throw for silent error command', async () => { await expect(async () => { await nvim.command('abcdefg') }).rejects.toThrow(/E492/) await nvim.command('silent! abcdefg') }) it('should use legacy eval', async () => { let res = await nvim.eval('"a"."b"') expect(res).toBe('ab') }) it('should get api info', async () => { let info = await nvim.apiInfo expect(typeof info[0]).toBe('number') }) it('should get buffer list', async () => { let bufs = await nvim.buffers expect(typeof bufs[0].id).toBe('number') }) it('should feedkeys', async () => { await nvim.setLine('foo') await nvim.feedKeys('$', 'int', false) let col = await nvim.call('col', ['.']) expect(col).toBe(3) await nvim.command('bd!') }) it('should list runtimepath', async () => { let res = await nvim.runtimePaths expect(Array.isArray(res)).toBe(true) }) it('should get command output', async () => { let res = await nvim.commandOutput('echo "foo"."bar"') expect(res).toMatch(/foobar/) await expect(async () => { await nvim.commandOutput('echonot_exists') }).rejects.toThrow(/E492/) }) it('should get line & set line', async () => { await nvim.setLine('foo') let curr = await nvim.getLine() expect(curr).toBe('foo') await nvim.deleteCurrentLine() curr = await nvim.getLine() expect(curr).toBe('') }) it('should get var', async () => { await nvim.setVar('foo', 'bar') let res = await nvim.getVar('foo') expect(res).toBe('bar') nvim.deleteVar('foo') res = await nvim.getVar('foo') expect(res).toBeNull() }) it('should get vvar', async () => { let res = await nvim.getVvar('progpath') expect(res).toMatch('vim') }) it('should get current buffer, window, tabpage', async () => { expect(await nvim.buffer).toBeDefined() expect(await nvim.window).toBeDefined() expect(await nvim.tabpage).toBeDefined() }) it('should get strwidth', async () => { let w = await nvim.strWidth('foo') expect(w).toBe(3) }) it('should out_write', async () => { nvim.outWrite('foo') nvim.outWriteLine('bar') let env = helper.workspace.env let line = await helper.getCmdline(env.lines - 1) expect(line).toBe('foobar') }) it('should err_write', async () => { nvim.errWrite('foo') nvim.errWriteLine('bar') let env = helper.workspace.env let line = await helper.getCmdline(env.lines - 1) expect(line).toBe('foobar') }) it('should create namespace', async () => { let ns = await nvim.createNamespace('foo') expect(typeof ns).toBe('number') let namespace = await nvim.createNamespace('foo') expect(ns).toBe(namespace) }) it('should add and delete keymap', async () => { nvim.setKeymap('n', ' ', ':normal! G', { nowait: true, script: true }) let res = await nvim.exec('nmap ', true) expect(res).toMatch('normal!') nvim.deleteKeymap('n', ' ') res = await nvim.exec('nmap ', true) expect(res).toMatch('No mapping found') }) }) describe('Buffer API', () => { let buffer: Buffer beforeEach(async () => { buffer = await nvim.buffer }) afterEach(async () => { await nvim.command('bd!') }) it('should checkLines on CursorHold', async () => { let doc = await helper.createDocument() let buffer = doc.buffer await buffer.setLines(['1', '2'], {}) await events.fire('CursorHold', [buffer.id, [1, 1]]) let called = false events.on('LinesChanged', bufnr => { if (bufnr == buffer.id) { called = true } }, null, disposables) Object.assign(doc, { lines: [''], _changedtick: doc.changedtick + 1 }) await events.fire('CursorHold', [buffer.id, [1, 1]]) expect(called).toBe(true) expect(doc.getLines()).toEqual(['1', '2']) }) it('should set buffer option', async () => { await buffer.setOption('buflisted', false) let curr = await buffer.getOption('buflisted') expect(curr).toBe(false) await buffer.setOption('buflisted', true) curr = await buffer.getOption('buflisted') expect(curr).toBe(true) }) it('should get changedtick', async () => { let changedtick = await buffer.changedtick let curr = await nvim.eval('b:changedtick') expect(changedtick).toBe(curr) }) it('should add and delete buffer keymap', async () => { buffer.setKeymap('n', 'e', ':normal! G', { noremap: true, nowait: true, silent: true }) let res = await nvim.exec('nmap e', true) expect(res).toMatch('normal!') buffer.deleteKeymap('n', 'e') res = await nvim.exec('nmap e', true) expect(res).toMatch('No mapping found') }) it('should check buffer valid', async () => { let valid = await buffer.valid expect(valid).toBe(true) let buf = nvim.createBuffer(99) valid = await buf.valid expect(valid).toBe(false) }) it('should get mark', async () => { await buffer.append(['', '', '']) let c = await buffer.length expect(c).toBe(4) await nvim.command(`normal! Gm"`) let m = await buffer.mark('"') expect(m).toEqual([4, 0]) await nvim.command('bd!') }) it('should add highlight', async () => { let ns = await nvim.createNamespace('test') as number await nvim.setLine('foo') let buf = await nvim.buffer await buf.addHighlight({ hlGroup: 'MoreMsg', line: 0, colStart: 0, colEnd: 3, srcId: ns }) let curr = await buf.getHighlights('test') expect(curr).toEqual([{ hlGroup: 'MoreMsg', lnum: 0, colStart: 0, colEnd: 3, id: 1001 }]) buf.clearNamespace(ns) curr = await buf.getHighlights('test') expect(curr).toEqual([]) }) it('should get line count', async () => { await buffer.append(['', '', '', '']) await nvim.command('tabe') let n = await buffer.length expect(n).toBe(5) await nvim.command('silent! %bwipeout!') await expect(async () => { let buf = nvim.createBuffer(-1) await buf.length }).rejects.toThrow(/Invalid buffer/) }) it('should get lines', async () => { await buffer.setLines(['1', '2', '3', '4'], { start: 0, end: -1, strictIndexing: false }) let lines = await buffer.lines expect(lines).toEqual(['1', '2', '3', '4']) lines = await buffer.getLines({ start: 0, end: 1, strictIndexing: false }) expect(lines).toEqual(['1']) lines = await buffer.getLines({ start: -2, end: -1, strictIndexing: false }) expect(lines).toEqual(['4']) await nvim.command('bd!') }) it('should set lines', async () => { // insert await buffer.setLines(['1', '2', '3'], { start: 0, end: 0, strictIndexing: true }) let lines = await buffer.lines expect(lines).toEqual(['1', '2', '3', '']) // replace await buffer.setLines(['4'], { start: 2, end: -1, strictIndexing: true }) lines = await buffer.lines expect(lines).toEqual(['1', '2', '4']) // delete await buffer.setLines([], { start: 1, end: 2, strictIndexing: true }) lines = await buffer.lines expect(lines).toEqual(['1', '4']) await buffer.setLines(['2', '3'], { start: 1, end: 2, strictIndexing: true }) lines = await buffer.lines expect(lines).toEqual(['1', '2', '3']) await nvim.command('bd!') }) it('should set name', async () => { await buffer.setName('foo') let name = await buffer.name expect(name).toBe('foo') await nvim.command('bd!') }) it('should change buffer variable', async () => { await buffer.setVar('foo', 'bar', false) let curr = await buffer.getVar('foo') expect(curr).toBe('bar') buffer.deleteVar('foo') curr = await buffer.getVar('foo') expect(curr).toBeNull() // another non-current buffer const buf2 = await nvim.createNewBuffer() await buf2.setVar('foo', 'qux', false) let curr2 = await buf2.getVar('foo') expect(curr2).toBe('qux') buf2.deleteVar('foo') curr = await buf2.getVar('foo') expect(curr).toBeNull() }) it('should add virtual text', async () => { let buf = await nvim.buffer await nvim.call('setline', ['.', ' foo']) let ns = await nvim.createNamespace('virtual-text') buf.setVirtualText(ns, 0, [['bar', 'MoreMsg']], { text_align: 'above', indent: true }) let types = await nvim.call('coc#api#GetNamespaceTypes', [ns]) let props = await nvim.call('prop_list', [1, { types }]) as any[] expect(props.length).toBe(1) let prop = props[0] if (featuredPropList) { expect(prop.text_align).toBe('above') expect(prop.text_padding_left).toBe(2) expect(prop.text).toBe('bar') } }) it('should set multiple virtual texts', async () => { let buf = await nvim.buffer let arr = (new Array(10)).fill('foo') await buf.setLines(arr) let ns = await nvim.createNamespace('vtext-set') let len = await buf.length let items: VirtualTextItem[] = [] for (let i = 0; i < len; i++) { items.push({ blocks: [[`${i}`, 'MoreMsg']], line: i, col: 1, right_gravity: true, virt_text_win_col: 0, hl_mode: 'blend' }) } await nvim.call('coc#vtext#set', [buf.id, ns, items, false, 900]) let types = await nvim.call('coc#api#GetNamespaceTypes', [ns]) let props = await nvim.call('prop_list', [1, { types, end_lnum: len }]) as any[] expect(props.length).toBe(10) let prop = props[0] expect(prop.lnum).toBe(1) expect(prop.col).toBe(1) if (featuredPropList) { expect(prop.text).toBe('0') } }) it('should update highlights', async () => { let buf = await nvim.buffer await buf.setLines(['foo', 'bar']) let hls = [] hls.push({ lnum: 0, colStart: 0, colEnd: 3, hlGroup: 'MoreMsg' }) hls.push({ lnum: 1, colStart: 1, colEnd: 3, hlGroup: 'MoreMsg' }) buf.updateHighlights('test', hls, { priority: 80 }) let arr = await buf.getHighlights('test') expect(arr.length).toBe(2) let obj = {} for (const key of ['hlGroup', 'lnum', 'colStart', 'colEnd']) { obj[key] = arr[0][key] } expect(obj).toEqual(hls[0]) await nvim.call('coc#highlight#clear_all', []) buf.updateHighlights('test', [hls[0]], { priority: 80, start: 0, end: 1 }) arr = await buf.getHighlights('test') expect(arr.length).toBe(1) let hl = { lnum: 1, colStart: 0, colEnd: -1, hlGroup: 'MoreMsg' } buf.updateHighlights('test', [hl], { priority: 80 }) arr = await buf.getHighlights('test') expect(arr.length).toBe(1) }) it('should highlight ranges', async () => { let buf = await nvim.buffer await buf.setLines(['foo', 'bar']) const range = Range.create(0, 0, 2, 0) buf.highlightRanges('test', 'MoreMsg', [range]) let arr = await buf.getHighlights('test') expect(arr.length).toBe(2) }) }) describe('Window API', () => { let win: Window beforeEach(async () => { win = await nvim.window }) it('should get buffer of window', async () => { let buf = await win.buffer let curr = await nvim.buffer expect(buf.id).toBe(curr.id) }) it('should set buffer', async () => { let bufnr = await nvim.call('bufadd', ['foo']) as number await nvim.call('bufload', [bufnr]) await win.setBuffer(nvim.createBuffer(bufnr)) let buf = await win.buffer expect(buf.id).toBe(bufnr) await nvim.command('silent! %bwipeout!') }) it('should get position', async () => { await nvim.command('sp') let res = await win.position expect(res[0]).toBeGreaterThan(0) expect(res[1]).toBe(0) await nvim.command('only!') }) it('should get and set height', async () => { let h = await win.height await win.setHeight(3) let curr = await win.height expect(curr).toBe(3) await win.setHeight(h) }) it('should get and set width', async () => { await nvim.command('vs') await win.setWidth(5) let curr = await win.width expect(curr).toBe(5) await nvim.command('only!') }) it('should get and set cursor', async () => { let buf = await nvim.buffer await buf.setLines(['1', '2', '3', '4'], { start: 0, end: -1, strictIndexing: false }) await win.setCursor([3, 1]) let cursor = await win.cursor expect(cursor).toEqual([3, 0]) await nvim.command('bd!') }) it('should get and set option', async () => { let relative = await win.getOption('relativenumber') expect(relative).toBe(false) await win.setOption('relativenumber', true) relative = await win.getOption('relativenumber') expect(relative).toBe(true) await win.setOption('relativenumber', false) await expect(async () => { await win.getOption('not_exists') }).rejects.toThrow('Invalid') await expect(async () => { await win.setOption('not_exists', '') }).rejects.toThrow('Invalid') }) it('should get and set var', async () => { await win.setVar('foo', 'bar') let curr = await win.getVar('foo') expect(curr).toBe('bar') let res = await win.getVar('not_exists') expect(res).toBeNull() win.deleteVar('foo') curr = await win.getVar('foo') expect(curr).toBe(null) }) it('should check window is valid', async () => { let valid = await win.valid expect(valid).toBe(true) let tab = await win.tabpage let nr = await tab.number expect(nr).toBe(1) let n = await win.number expect(n).toBe(1) await nvim.command('vs') await nvim.call('win_gotoid', [win.id]) await win.close(true) valid = await win.valid expect(valid).toBe(false) await nvim.command('only!') }) it('should add and clear matches', async () => { let buf = await nvim.buffer let arr = new Array(10) arr.fill('foo') await buf.setLines(arr) let ranges: Range[] = [] for (let i = 0; i < 10; i++) { ranges.push(Range.create(i, 0, i, 3)) } let win = await nvim.window let ids = await win.highlightRanges('MoreMsg', ranges) expect(ids.length).toBeGreaterThan(0) let matches = await helper.getMatches('MoreMsg') expect(matches.length).toBe(10) win.clearMatches(ids) matches = await helper.getMatches('MoreMsg') expect(matches.length).toBe(0) }) }) describe('Popup', () => { it('should works for popup window', async () => { let winid = await nvim.call('popup_create', [['foo', 'bar'], {}]) as number expect(winid).toBeGreaterThan(1000) let win = nvim.createWindow(winid) let buf = await win.buffer expect(buf.id).toBeGreaterThan(0) let pos = await win.position expect(typeof pos[0]).toBe('number') expect(typeof pos[1]).toBe('number') await win.setHeight(10) let height = await win.height expect(height).toBe(10) await win.setWidth(20) let width = await win.width expect(width).toBe(20) await win.setCursor([1, 2]) let cur = await win.cursor expect(cur).toEqual([1, 2]) await win.setOption('relativenumber', true) // different on neovim which returns true and false let option = await win.getOption('relativenumber') expect(option).toBe(true) await win.setVar('foo', 'bar', false) let val = await win.getVar('foo') expect(val).toBe('bar') win.deleteVar('foo') val = await win.getVar('foo') expect(val).toBeNull() let valid = await win.valid expect(valid).toBe(true) // not work on vim let num = await win.number expect(num).toBe(0) let tabpage = await win.tabpage expect(tabpage.id).toBeGreaterThan(0) await win.close(true) await nvim.call('popup_clear', []) }) it('should create inputBox', async () => { let input = await helper.plugin.window.createInputBox('title', '') input.title = 'new title' let curr: string input.onDidChange(text => { curr = text }) await nvim.input('abc') await helper.waitValue((() => { return curr }), 'abc') input.dispose() }) }) describe('Tabpage API', () => { let tab: Tabpage beforeEach(async () => { tab = await nvim.tabpage }) it('should get window list', async () => { await nvim.command('vs') let wins = await tab.windows expect(wins.length).toBe(2) await nvim.command('only!') }) it('should get and set var', async () => { await tab.setVar('foo', 'bar') let curr = await tab.getVar('foo') expect(curr).toBe('bar') tab.deleteVar('foo') curr = await tab.getVar('foo') expect(curr).toBe(null) }) it('should get current window', async () => { let valid = await tab.valid expect(valid).toBe(true) let win = await tab.window let curr = await nvim.call('win_getid') expect(win.id).toBe(curr) }) }) describe('notify', () => { it('should call function by notify', async () => { let curr = await nvim.call('line', ['.']) nvim.call('setline', [curr, 'foo'], true) await helper.waitValue(async () => { return await nvim.call('getline', [curr]) }, 'foo') await nvim.command('normal! dd') }) }) describe('document', () => { async function shouldEqual(doc, synced = false): Promise { let lines = synced ? doc.textDocument.lines : doc.getLines() let cur = await doc.buffer.lines expect(lines).toEqual(cur) } it('should synchronize current buffer when call vim function', async () => { let doc = await helper.createDocument() await nvim.call('appendbufline', [doc.bufnr, 0, ['3', '4', '5']]) await nvim.call('setbufline', [doc.bufnr, 1, 'txt']) await shouldEqual(doc) }) it('should synchronize changes', async () => { let lines = [] for (let i = 1; i < 8; i++) { lines.push(`line ${i}`) } let filepath = await createTmpFile(lines.join('\n'), disposables) let doc = await helper.createDocument(filepath) let bufnr = doc.buffer.id // remove first line nvim.pauseNotification() nvim.call('deletebufline', [bufnr, 1, 3], true) nvim.call('appendbufline', [bufnr, 0, ['3', '4', '5']], true) await nvim.resumeNotification(true) await shouldEqual(doc) await doc.patchChange() }) it('should patch change of current line', async () => { let doc = await helper.createDocument() nvim.call('setline', ['.', 'foo'], true) await doc.patchChange() await shouldEqual(doc, true) nvim.call('setline', ['.', 'foo'], true) await doc.patchChange() await shouldEqual(doc, true) }) it('should patch change', async () => { let doc = await helper.workspace.document // synchronize after user input await nvim.input('o') await doc.patchChange() let buf = doc.buffer // synchronize after api buf.setLines(['aa', 'bb'], { start: 0, end: 1, strictIndexing: false }, true) await doc.patchChange() await shouldEqual(doc) await nvim.deleteCurrentLine() await shouldEqual(doc) await nvim.setLine('foo') await shouldEqual(doc) await nvim.command('stopinsert') }) it('should synchronize after changeLines', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['a', 'b', 'c', 'd']) await doc.synchronize() await doc.changeLines([ [0, 'd'], [1, 'c'], [2, 'b'], [3, 'a'], ]) await shouldEqual(doc) }) it('should add and remove lines', async () => { let doc = await helper.workspace.document await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\n')]) await shouldEqual(doc) await doc.applyEdits([TextEdit.replace(Range.create(0, 0, 3, 0), '')]) await shouldEqual(doc) await nvim.command('bd!') }) it('should synchronize hidden buffer after replace lines', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['a', 'b', 'c', 'd']) await nvim.command('enew') await shouldEqual(doc) await doc.applyEdits([TextEdit.replace(Range.create(0, 0, 4, 0), 'c\nb\na\n')]) await doc.patchChange() await shouldEqual(doc) await nvim.command('bd!') }) async function assertBuffer(lines: string[], hls: [string, number, number, number][]): Promise { let buf = await nvim.buffer let curr = await buf.lines expect(curr).toEqual(lines) let highlights = await buf.getHighlights('test') let arr = highlights.map(o => [o.hlGroup, o.lnum, o.colStart, o.colEnd]) expect(arr).toEqual(hls) } it('should apply single line edit', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['foo foo']) await doc.patchChange() let ranges = [Range.create(0, 0, 0, 3), Range.create(0, 4, 0, 7)] doc.buffer.highlightRanges('test', 'MoreMsg', ranges) let edit = TextEdit.replace(Range.create(0, 3, 0, 4), 'xy') await doc.applyEdits([edit]) await assertBuffer(['fooxyfoo'], [ ['MoreMsg', 0, 0, 3], ['MoreMsg', 0, 5, 8], ]) edit = TextEdit.replace(Range.create(0, 1, 0, 7), '') await doc.applyEdits([edit]) await assertBuffer(['fo'], []) await doc.buffer.append(['bar']) await doc.patchChange() ranges = [Range.create(0, 0, 0, 1), Range.create(1, 2, 1, 3)] doc.buffer.highlightRanges('test', 'MoreMsg', ranges) edit = TextEdit.replace(Range.create(0, 1, 1, 2), 'x') await doc.applyEdits([edit]) await doc.patchChange() await assertBuffer(['fxr'], [ ['MoreMsg', 0, 0, 1], ['MoreMsg', 0, 2, 3], ]) }) it('should apply multi lines edit', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['foo foo']) await doc.patchChange() let ranges = [Range.create(0, 0, 0, 3), Range.create(0, 4, 0, 7)] doc.buffer.highlightRanges('test', 'MoreMsg', ranges) let edit = TextEdit.replace(Range.create(0, 3, 0, 4), 'a\nb\nc') await doc.applyEdits([edit]) await assertBuffer(['fooa', 'b', 'cfoo'], [ ['MoreMsg', 0, 0, 3], ['MoreMsg', 2, 1, 4], ]) edit = TextEdit.replace(Range.create(0, 3, 2, 1), '\n') await doc.applyEdits([edit]) await assertBuffer(['foo', 'foo'], [ ['MoreMsg', 0, 0, 3], ['MoreMsg', 1, 0, 3], ]) }) it('should apply for lines replace edit', async () => { let doc = await helper.createDocument() await doc.buffer.setLines(['foo', 'bar']) await doc.patchChange() let edit = TextEdit.replace(Range.create(0, 0, 1, 0), 'a\nb\n') await doc.applyEdits([edit, TextEdit.insert(Position.create(1, 0), 'x')]) let lines = await doc.buffer.lines expect(lines).toEqual(['a', 'b', 'xbar']) edit = TextEdit.replace(Range.create(0, 0, 2, 0), '') await doc.applyEdits([edit, TextEdit.replace(Range.create(2, 0, 2, 1), '')]) lines = await doc.buffer.lines expect(lines).toEqual(['bar']) }) it('should apply multiple edits', async () => { let doc = await helper.createDocument() let arr = new Array(10) arr.fill('foo bar a b c d e') let ranges: Range[] = [] let edits: TextEdit[] = [] for (let i = 0; i < arr.length; i++) { ranges.push(Range.create(i, 0, i, 3)) ranges.push(Range.create(i, 4, i, 7)) ranges.push(Range.create(i, 8, i, 9)) ranges.push(Range.create(i, 10, i, 11)) ranges.push(Range.create(i, 12, i, 13)) ranges.push(Range.create(i, 14, i, 15)) ranges.push(Range.create(i, 16, i, 17)) edits.push(TextEdit.insert(Position.create(i, 0), `${i + 1} `)) } let buf = doc.buffer await buf.setLines(arr) buf.highlightRanges('test', 'Title', ranges) await doc.synchronize() await doc.applyEdits(edits) await events.race(['TextChanged'], 200) let hls = await buf.getHighlights('test') expect(hls.length).toBe(70) }) it('should consider latest change', async () => { let doc = await helper.createDocument() let buf = doc.buffer { let edits: TextEdit[] = [TextEdit.insert(Position.create(0, 0), 'bar')] nvim.call('setline', [1, 'foo'], true) await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe('foobar') } { await buf.setLines([' foo']) await doc.patchChange() nvim.call('setline', [1, ' fooa'], true) nvim.call('cursor', [1, 7], true) let edits: TextEdit[] = [TextEdit.del(Range.create(0, 0, 0, 1))] await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe(' fooa') } { await buf.setLines(['foo']) await nvim.call('cursor', [1, 3]) await doc.synchronize() nvim.call('setline', [1, 'fo'], true) let edits: TextEdit[] = [TextEdit.insert(Position.create(0, 0), ' ')] await doc.applyEdits(edits) let line = await nvim.line expect(line).toBe(' fo') } }) }) ================================================ FILE: src/__tests__/vimrc ================================================ set nocompatible set hidden set noswapfile set nobackup set tabstop=2 set cmdheight=2 set shiftwidth=2 set updatetime=300 set expandtab set noshowmode set shortmess=aFtW set noruler let s:dir = expand(':h') let s:root = expand(':h:h:h') let g:coc_node_env = 'test' execute 'set runtimepath+='.s:root call coc#add_command('config', 'let g:coc_config_init = 1') " Float window id on current tab. function! GetFloatWin() abort if has('nvim') for i in range(1, winnr('$')) let id = win_getid(i) let config = nvim_win_get_config(id) if (!empty(config) && config['focusable'] == v:true && !empty(config['relative'])) if !getwinvar(id, 'button', 0) return id endif endif endfor else let ids = popup_list() return get(filter(ids, 'get(popup_getpos(v:val),"visible",0)'), 0, 0) endif return 0 endfunction " float/popup relative to current cursor position function! GetFloatCursorRelative(winid) abort if !coc#float#valid(a:winid) return v:null endif let winid = win_getid() if winid == a:winid return v:null endif let [cursorLine, cursorCol] = coc#cursor#screen_pos() if has('nvim') let [row, col] = nvim_win_get_position(a:winid) return {'row' : row - cursorLine, 'col' : col - cursorCol} endif let pos = popup_getpos(a:winid) return {'row' : pos['line'] - cursorLine - 1, 'col' : pos['col'] - cursorCol - 1} endfunction " fake clipboard let g:clipboard = { \ 'name': 'fakeClipboard', \ 'copy': { \ '+': {lines, regtype -> extend(g:, {'fakeClipboard': [lines, regtype]}) }, \ '*': {lines, regtype -> extend(g:, {'fakeClipboard': [lines, regtype]}) }, \ }, \ 'paste': { \ '+': {-> get(g:, 'fakeClipboard', [])}, \ '*': {-> get(g:, 'fakeClipboard', [])}, \ }, \} ================================================ FILE: src/attach.ts ================================================ 'use strict' import { attach, Attach, Neovim } from '@chemzqm/neovim' import events from './events' import { createLogger } from './logger' import Plugin from './plugin' import { VERSION } from './util/constants' import { semver } from './util/node' import { toErrorText } from './util/string' import { createTiming } from './util/timing' const logger = createLogger('attach') /** * Request actions that not need plugin ready */ const ACTIONS_NO_WAIT = ['installExtensions', 'updateExtensions'] const semVer = semver.parse(VERSION) let pendingNotifications: [string, any[]][] = [] const NO_ERROR_REQUEST = ['doAutocmd', 'CocAutocmd'] export default (opts: Attach, requestApi = false): Plugin => { const nvim: Neovim = attach(opts, createLogger('node-client'), requestApi) nvim.setVar('coc_process_pid', process.pid, true) nvim.setClientInfo('coc', { major: semVer.major, minor: semVer.minor, patch: semVer.patch }, 'remote', {}, {}) const plugin = new Plugin(nvim) let disposable = events.on('ready', () => { disposable.dispose() for (let [method, args] of pendingNotifications) { plugin.cocAction(method, ...args).catch(e => { console.error(`Error on notification "${method}": ${e}`) logger.error(`Error on notification ${method}`, e) }) } pendingNotifications = [] }) nvim.on('notification', async (method, args) => { switch (method) { case 'VimEnter': { await plugin.init(args[0]) break } case 'Log': { logger.debug('Vim log', ...args) break } case 'TaskExit': case 'TaskStderr': case 'TaskStdout': case 'GlobalChange': case 'PromptInsert': case 'PromptExit': case 'InputChar': case 'MenuInput': case 'OptionSet': case 'PromptKeyPress': case 'FloatBtnClick': case 'InputListSelect': case 'PumNavigate': logger.trace('Event: ', method, ...args) await events.fire(method, args) break case 'CocAutocmd': logger.trace('Notification autocmd:', ...args) await events.fire(args[0], args.slice(1)) break case 'redraw': break default: { try { logger.info('receive notification:', method, args) if (!plugin.isReady) { pendingNotifications.push([method, args]) return } await plugin.cocAction(method, ...args) } catch (e) { console.error(`Error on notification "${method}": ${toErrorText(e)}`) logger.error(`Error on notification ${method}`, e) } } } }) let timing = createTiming('Request', 3000) nvim.on('request', async (method: string, args, resp) => { timing.start(method) try { events.requesting = true if (method == 'CocAutocmd') { logger.trace('Request autocmd:', ...args) await events.fire(args[0], args.slice(1)) resp.send(undefined) } else { if (!plugin.isReady && !ACTIONS_NO_WAIT.includes(method)) { logger.warn(`Plugin not ready on request "${method}"`, args) resp.send('Plugin not ready', true) } else { logger.info('Request action:', method, args) let res = await plugin.cocAction(method, ...args) resp.send(res) } } events.requesting = false } catch (e) { events.requesting = false // Avoid autocmd request failure if (NO_ERROR_REQUEST.includes(method)) { nvim.echoError(new Error(`Request "${method}" failed`)) resp.send('') } else { resp.send(toErrorText(e), true) } logger.error(`Request error:`, method, args, e) } timing.stop() }) return plugin } ================================================ FILE: src/commands.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Command as VCommand } from 'vscode-languageserver-types' import events from './events' import { createLogger } from './logger' import Mru from './model/mru' import { toArray } from './util/array' import { Extensions as ExtensionsInfo, IExtensionRegistry } from './util/extensionRegistry' import { Disposable } from './util/protocol' import { Registry } from './util/registry' import { toText } from './util/string' const logger = createLogger('commands') // command center export interface Command { readonly id: string | string[] execute(...args: any[]): void | Promise } class CommandItem implements Disposable, Command { constructor( public id: string, private impl: (...args: any[]) => void, private thisArg: any, public internal: boolean ) { } public execute(...args: any[]): void | Promise { let { impl, thisArg } = this return impl.apply(thisArg, toArray(args)) } public dispose(): void { this.thisArg = null this.impl = null } } const extensionRegistry = Registry.as(ExtensionsInfo.ExtensionContribution) class CommandManager implements Disposable { private readonly commands = new Map() public titles = new Map() private mru = new Mru('commands') public nvim: Neovim public get commandList(): { id: string, title: string }[] { let res: { id: string, title: string }[] = [] for (let item of this.commands.values()) { if (!item.internal) { let { id } = item let title = this.titles.get(id) ?? extensionRegistry.getCommandTitle(id) res.push({ id, title: toText(title) }) } } return res } public dispose(): void { for (const registration of this.commands.values()) { registration.dispose() } this.commands.clear() } public execute(command: VCommand): Promise { return this.executeCommand(command.command, ...(command.arguments ?? [])) } public register(command: T, internal: boolean, description?: string): T { for (const id of Array.isArray(command.id) ? command.id : [command.id]) { this.registerCommand(id, command.execute, command, internal) if (description) this.titles.set(id, description) } return command } public has(id: string): boolean { return this.commands.has(id) } public unregister(id: string): void { let item = this.commands.get(id) if (!item) return item.dispose() this.commands.delete(id) } /** * Registers a command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. * * Registering a command with an existing command identifier twice * will cause an error. * @param command A unique identifier for the command. * @param impl A command handler function. * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ public registerCommand(id: string, impl: (...args: any[]) => T | Promise, thisArg?: any, internal = false): Disposable { if (id.startsWith("_")) internal = true if (this.commands.has(id)) logger.warn(`Command ${id} already registered`) this.commands.set(id, new CommandItem(id, impl, thisArg, internal)) return Disposable.create(() => { this.commands.delete(id) }) } /** * Executes the command denoted by the given command identifier. * * * *Note 1:* When executing an editor command not all types are allowed to * be passed as arguments. Allowed are the primitive types `string`, `boolean`, * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`URI`](#URI) and [`Location`](#Location). * * *Note 2:* There are no restrictions when executing commands that have been contributed * by extensions. * @param command Identifier of the command to execute. * @param rest Parameters passed to the command function. * @return A promise that resolves to the returned value of the given command. `undefined` when * the command handler function doesn't return anything. */ public executeCommand(command: string, ...rest: any[]): Promise { let cmd = this.commands.get(command) if (!cmd) throw new Error(`Command: ${command} not found`) return Promise.resolve(cmd.execute.apply(cmd, rest)) } /** * Used for user invoked command. */ public async fireCommand(id: string, ...args: any[]): Promise { // needed to load onCommand extensions await events.fire('Command', [id]) let start = Date.now() let res = await this.executeCommand(id, ...args) if (args.length == 0) { await this.addRecent(id, events.lastChangeTs > start) } return res } public async addRecent(cmd: string, repeat: boolean): Promise { await this.mru.add(cmd) if (repeat) this.nvim.command(`silent! call repeat#set("\\(coc-command-repeat)", -1)`, true) } public async repeatCommand(): Promise { let mruList = await this.mru.load() let first = mruList[0] if (first) { await this.executeCommand(first) await this.nvim.command(`silent! call repeat#set("\\(coc-command-repeat)", -1)`) } } } export default new CommandManager() ================================================ FILE: src/completion/complete.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import { Position, Range } from 'vscode-languageserver-types' import { createLogger } from '../logger' import type Document from '../model/document' import { waitWithToken } from '../util' import { isFalsyOrEmpty } from '../util/array' import { anyScore, FuzzyScore, fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer } from '../util/filter' import * as Is from '../util/is' import { clamp } from '../util/numbers' import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from '../util/protocol' import { characterIndex } from '../util/string' import workspace from '../workspace' import { CompleteConfig, CompleteItem, CompleteOption, DurationCompleteItem, InsertMode, ISource, SortMethod } from './types' import { Converter, ConvertOption, getPriority, useAscii } from './util' import { WordDistance } from './wordDistance' const logger = createLogger('completion-complete') const MAX_DISTANCE = 2 << 20 const MIN_TIMEOUT = 50 const MAX_TIMEOUT = 15000 const MAX_TRIGGER_WAIT = 200 const WORD_SOURCES = new Set(['buffer', 'around', 'word']) export interface CompleteResultToFilter { items: DurationCompleteItem[] isIncomplete?: boolean } export default class Complete { // identify this complete private results: Map = new Map() private _input = '' private _completing = false private timer: NodeJS.Timeout private names: string[] = [] private asciiMatch: boolean private timeout: number private cid = 0 private minCharacter = Number.MAX_SAFE_INTEGER private inputStart: number private completingSources: Set = new Set() private readonly _onDidRefresh = new Emitter() private wordDistance: WordDistance | undefined private tokenSources: Set = new Set() private tokensInfo: WeakMap = new WeakMap() private itemsMap: WeakMap = new WeakMap() public readonly onDidRefresh: Event = this._onDidRefresh.event constructor(public option: CompleteOption, public readonly document: Document, private config: CompleteConfig, private sources: ISource[]) { this.inputStart = characterIndex(option.line, option.col) this.timeout = clamp(this.config.timeout, MIN_TIMEOUT, MAX_TIMEOUT) sources.sort((a, b) => (b.priority ?? 99) - (a.priority ?? 99)) this.names = sources.map(o => o.name) this.asciiMatch = config.asciiMatch && useAscii(option.input) } private get nvim(): Neovim { return workspace.nvim } // trigger texts starts at character public getTrigger(character: number): string { let { linenr, col } = this.option let line = this.document.getline(linenr - 1) let pre = line.slice(0, characterIndex(line, col)) + this.input return pre.slice(character) } private fireRefresh(waitTime: number): void { clearTimeout(this.timer) if (!waitTime) { // Needed to wait this._completing = false process.nextTick(() => { this._onDidRefresh.fire() }) } else { this.timer = setTimeout(() => { this._onDidRefresh.fire() }, waitTime) } } private get totalLength(): number { let len = 0 for (let result of this.results.values()) { len += result.items.length } return len } public resolveItem(item: DurationCompleteItem | undefined): { source: ISource, item: CompleteItem } | undefined { if (!item) return undefined return { source: item.source, item: this.itemsMap.get(item) } } public get isCompleting(): boolean { return this._completing } public get input(): string { return this._input } public get isEmpty(): boolean { return this.results.size === 0 } private get hasInComplete(): boolean { for (let result of this.results.values()) { if (result.isIncomplete) return true } return false } public getIncompleteSources(): ISource[] { return this.sources.filter(s => { let res = this.results.get(s.name) return res && res.isIncomplete === true }) } public async doComplete(): Promise { let tokenSource = this.createTokenSource(false) let token = tokenSource.token let res = await Promise.all([ this.nvim.call('coc#util#synname', []), this.nvim.call('coc#_suggest_variables', []), this.document.patchChange() ]) as [string, { disable: boolean, disabled_sources: string[], blacklist: string[] }, undefined] if (token.isCancellationRequested) return this.option.synname = res[0] let variables = res[1] if (variables.disable) { logger.warn('suggest cancelled by b:coc_suggest_disable') return true } if (!isFalsyOrEmpty(variables.disabled_sources)) { this.sources = this.sources.filter(s => !variables.disabled_sources.includes(s.name)) if (this.sources.length === 0) { logger.warn('suggest cancelled by b:coc_disabled_sources') return true } } if (!isFalsyOrEmpty(variables.blacklist) && variables.blacklist.includes(this.option.input)) { logger.warn('suggest cancelled by b:coc_suggest_blacklist') return true } void WordDistance.create(this.config.localityBonus, this.option, token).then(instance => { this.wordDistance = instance }) await waitWithToken(clamp(this.config.triggerCompletionWait, 0, MAX_TRIGGER_WAIT), tokenSource.token) await this.completeSources(this.sources, tokenSource, this.cid) } private async completeSources(sources: ReadonlyArray, tokenSource: CancellationTokenSource, cid: number): Promise { const token = tokenSource.token if (token.isCancellationRequested) return this._completing = true const remains: Set = new Set(sources.map(s => s.name)) let timer: NodeJS.Timeout let disposable: Disposable let tp = new Promise(resolve => { disposable = token.onCancellationRequested(() => { clearTimeout(timer) resolve() }) timer = setTimeout(() => { let names = Array.from(remains) disposable.dispose() tokenSource.cancel() logger.warn(`Completion timeout after ${this.timeout}ms`, names) this.nvim.setVar(`coc_timeout_sources`, names, true) resolve() }, this.timeout) }) // default insert or replace range const range = this.getDefaultRange() let promises = sources.map(s => this.completeSource(s, range, token).then(added => { remains.delete(s.name) if (token.isCancellationRequested) return if (this.completingSources.size === 0) { this.fireRefresh(0) } else if (added) { this.fireRefresh(16) } })) await Promise.race([tp, Promise.allSettled(promises)]) this.tokenSources.delete(tokenSource) disposable.dispose() clearTimeout(timer) if (cid === this.cid) this._completing = false } private async completeSource(source: ISource, range: Range, token: CancellationToken): Promise { // new option for each source let opt = Object.assign({}, this.option) let { asciiMatch } = this const insertMode = this.config.insertMode const sourceName = source.name let added = false this.completingSources.add(sourceName) try { if (Is.func(source.shouldComplete)) { let shouldRun = await Promise.resolve(source.shouldComplete(opt)) if (!shouldRun || token.isCancellationRequested) return } const start = Date.now() const map = this.itemsMap await new Promise((resolve, reject) => { Promise.resolve(source.doComplete(opt, token)).then(result => { if (token.isCancellationRequested) { resolve(undefined) return } let len = result ? result.items.length : 0 logger.debug(`Source "${sourceName}" finished with ${len} items ms cost:`, Date.now() - start) if (len > 0) { if (Is.number(result.startcol)) { let line = opt.linenr - 1 range = Range.create(line, characterIndex(opt.line, result.startcol), line, range.end.character) } const priority = getPriority(source, this.config.languageSourcePriority) const option: ConvertOption = { source, insertMode, priority, asciiMatch, itemDefaults: result.itemDefaults, range } const converter = new Converter(this.inputStart, option, opt) const items = result.items.reduce((items, item) => { let completeItem = converter.convertToDurationItem(item) if (!completeItem) { logger.error(`Unexpected completion item from ${sourceName}:`, item) return items } map.set(completeItem, item) items.push(completeItem) return items }, []) this.minCharacter = Math.min(this.minCharacter, converter.minCharacter) this.results.set(sourceName, { items, isIncomplete: result.isIncomplete === true }) added = true } else { this.results.delete(sourceName) } resolve() }, (err: Error) => { reject(err) }) }) } catch (err) { // this.nvim.echoError(err) logger.error('Complete error:', source.name, err) } this.completingSources.delete(sourceName) return added } public async completeInComplete(resumeInput: string): Promise { let { document } = this this.cancelInComplete() let tokenSource = this.createTokenSource(true) await document.patchChange() let { input, colnr, linenr, followWord, position } = this.option Object.assign(this.option, { word: resumeInput + followWord, input: resumeInput, line: document.getline(linenr - 1), position: { line: position.line, character: position.character + resumeInput.length - input.length }, colnr: colnr + (resumeInput.length - input.length), triggerCharacter: undefined, triggerForInComplete: true }) this.cid++ const sources = this.getIncompleteSources() await this.completeSources(sources, tokenSource, this.cid) } public filterItems(input: string): DurationCompleteItem[] | undefined { let { results, names, option, inputStart } = this this._input = input let len = input.length let { maxItemCount, defaultSortMethod, removeDuplicateItems, removeCurrentWord } = this.config let arr: DurationCompleteItem[] = [] let words: Set = new Set() const emptyInput = len == 0 const lowInput = input.toLowerCase() const scoreFn: FuzzyScorer = (!this.config.filterGraceful || this.totalLength > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive const scoreOption = { boostFullMatch: true, firstMatchCanBeWeak: false } const anchor = Position.create(option.linenr - 1, inputStart) for (let name of names) { let result = results.get(name) if (!result) continue let isWord = WORD_SOURCES.has(name) let items = result.items for (let idx = 0; idx < items.length; idx++) { let item = items[idx] let { word, filterText, dup } = item if (dup !== true && words.has(word)) continue if (removeCurrentWord && isWord && word === input) continue if (removeDuplicateItems && item.isSnippet !== true && words.has(word)) continue let fuzzyResult: FuzzyScore | undefined if (!emptyInput) { scoreOption.firstMatchCanBeWeak = item.delta === 0 && item.character !== inputStart if (item.delta > 0) { // better input to make it have higher score and better highlight let prev = filterText.slice(0, item.delta) fuzzyResult = scoreFn(prev + input, prev.toLowerCase() + lowInput, 0, filterText, filterText.toLowerCase(), 0, scoreOption) } else { fuzzyResult = scoreFn(input, lowInput, 0, filterText, filterText.toLowerCase(), 0, scoreOption) } if (fuzzyResult == null) continue item.score = fuzzyResult[0] item.positions = fuzzyResult if (this.wordDistance) item.localBonus = MAX_DISTANCE - this.wordDistance.distance(anchor, item) } else if (item.character < inputStart) { let trigger = option.line.slice(item.character, inputStart) scoreOption.firstMatchCanBeWeak = true fuzzyResult = anyScore(trigger, trigger.toLowerCase(), 0, filterText, filterText.toLowerCase(), 0, scoreOption) item.score = fuzzyResult[0] item.positions = fuzzyResult } else { item.score = 0 item.positions = undefined } words.add(word) arr.push(item) } } arr.sort(sortItems.bind(null, emptyInput, defaultSortMethod)) return this.limitCompleteItems(arr.slice(0, maxItemCount)) } public async filterResults(input: string): Promise { if (input.length > this.option.input.length && this.hasInComplete) { this.fireRefresh(30) void this.completeInComplete(input) return undefined } clearTimeout(this.timer) return this.filterItems(input) } private limitCompleteItems(items: DurationCompleteItem[]): DurationCompleteItem[] { let { highPrioritySourceLimit, lowPrioritySourceLimit } = this.config if (!highPrioritySourceLimit && !lowPrioritySourceLimit) return items let counts: Map = new Map() return items.filter(item => { let { priority, source } = item let isLow = priority < 90 let curr = counts.get(source) || 0 if ((lowPrioritySourceLimit && isLow && curr == lowPrioritySourceLimit) || (highPrioritySourceLimit && !isLow && curr == highPrioritySourceLimit)) { return false } counts.set(source, curr + 1) return true }) } private getDefaultRange(): Range { let { insertMode } = this.config let { linenr, followWord, position } = this.option let line = linenr - 1 let end = position.character + (insertMode == InsertMode.Replace ? followWord.length : 0) return Range.create(line, this.inputStart, line, end) } private createTokenSource(isIncomplete: boolean): CancellationTokenSource { let tokenSource = new CancellationTokenSource() this.tokenSources.add(tokenSource) tokenSource.token.onCancellationRequested(() => { this.tokenSources.delete(tokenSource) }) this.tokensInfo.set(tokenSource, isIncomplete) return tokenSource } private cancelInComplete(): void { let { tokenSources, tokensInfo } = this for (let tokenSource of Array.from(tokenSources)) { if (tokensInfo.get(tokenSource) === true) { tokenSource.cancel() } } } public cancel(): void { let { tokenSources, timer } = this clearTimeout(timer) for (let tokenSource of Array.from(tokenSources)) { tokenSource.cancel() } tokenSources.clear() this._completing = false } public dispose(): void { this.cancel() this.results.clear() this._onDidRefresh.dispose() } } export function sortItems(emptyInput: boolean, defaultSortMethod: SortMethod, a: DurationCompleteItem, b: DurationCompleteItem): number { let sa = a.sortText let sb = b.sortText if (a.score !== b.score) return b.score - a.score if (a.priority !== b.priority) return b.priority - a.priority if (a.source === b.source && sa !== sb) return sa < sb ? -1 : 1 if (a.localBonus !== b.localBonus) return b.localBonus - a.localBonus // not sort with empty input, the item not replace trigger have higher priority if (emptyInput) return b.character - a.character switch (defaultSortMethod) { case SortMethod.None: return 0 case SortMethod.Alphabetical: return a.filterText.localeCompare(b.filterText) case SortMethod.Length: default: // Fallback on length return a.filterText.length - b.filterText.length } } ================================================ FILE: src/completion/floating.ts ================================================ 'use strict' import { createLogger } from '../logger' import { parseDocuments } from '../markdown' import { Documentation, FloatConfig } from '../types' import { getConditionValue } from '../util' import { CancellationError, isCancellationError } from '../util/errors' import * as Is from '../util/is' import { CancellationToken, CancellationTokenSource } from '../util/protocol' import workspace from '../workspace' import { CompleteItem, CompleteOption, ISource } from './types' import { getDocumentations } from './util' const logger = createLogger('completion-floating') const RESOLVE_TIMEOUT = getConditionValue(500, 50) export default class Floating { private resolveTokenSource: CancellationTokenSource | undefined constructor(private config: { floatConfig: FloatConfig }) { } public async resolveItem(source: ISource, item: CompleteItem, opt: CompleteOption, showDocs: boolean, detailRendered = false): Promise { this.cancel() if (Is.func(source.onCompleteResolve)) { try { await this.requestWithToken(token => { return Promise.resolve(source.onCompleteResolve(item, opt, token)) }) } catch (e) { if (isCancellationError(e)) return logger.error(`Error on resolve complete item from ${source.name}:`, item, e) // not return, may need show/hide docs. } } if (showDocs) { this.show(getDocumentations(item, opt.filetype, detailRendered)) } } public show(docs: Documentation[]): void { let config = this.config.floatConfig docs = docs.filter(o => o.content.trim().length > 0) if (docs.length === 0) { this.close() } else { const markdownPreference = workspace.configurations.markdownPreference let { lines, codes, highlights } = parseDocuments(docs, markdownPreference) let opts: any = { codes, highlights, highlight: config.highlight ?? 'CocFloating', maxWidth: config.maxWidth || 80, rounded: config.rounded ? 1 : 0, focusable: config.focusable === true ? 1 : 0 } if (Is.string(config.title)) opts.title = config.title if (config.shadow) opts.shadow = 1 if (config.border) opts.border = [1, 1, 1, 1] if (config.borderhighlight) opts.borderhighlight = config.borderhighlight if (typeof config.winblend === 'number') opts.winblend = config.winblend let { nvim } = workspace nvim.call('coc#dialog#create_pum_float', [lines, opts], true) nvim.redrawVim() } } public close(): void { workspace.nvim.call('coc#pum#close_detail', [], true) workspace.nvim.redrawVim() } public cancel(): void { if (this.resolveTokenSource) { this.resolveTokenSource.cancel() this.resolveTokenSource = undefined } } private requestWithToken(fn: (token: CancellationToken) => Promise): Promise { let tokenSource = this.resolveTokenSource = new CancellationTokenSource() return new Promise((resolve, reject) => { let called = false let onFinish = (err?: Error) => { if (called) return called = true disposable.dispose() clearTimeout(timer) if (this.resolveTokenSource === tokenSource) { this.resolveTokenSource = undefined } if (err) { reject(err) } else { resolve() } } let timer = setTimeout(() => { tokenSource.cancel() }, RESOLVE_TIMEOUT) let disposable = tokenSource.token.onCancellationRequested(() => { onFinish(new CancellationError()) }) fn(tokenSource.token).then(() => { onFinish() }, e => { onFinish(e) }) }) } } ================================================ FILE: src/completion/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Position, Range, SelectedCompletionInfo } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import type { IConfigurationChangeEvent } from '../configuration/types' import events, { InsertChange, PopupChangeEvent } from '../events' import { createLogger } from '../logger' import type Document from '../model/document' import { defaultValue, disposeAll, getConditionValue, pariedCharacters } from '../util' import { isFalsyOrEmpty, toArray } from '../util/array' import { onUnexpectedError } from '../util/errors' import * as Is from '../util/is' import { debounce } from '../util/node' import { toNumber } from '../util/numbers' import { toObject } from '../util/object' import type { Disposable } from '../util/protocol' import { byteIndex, byteLength, byteSlice, toText } from '../util/string' import window from '../window' import workspace from '../workspace' import Complete from './complete' import Floating from './floating' import PopupMenu, { PopupMenuConfig } from './pum' import sources from './sources' import { CompleteConfig, CompleteDoneOption, CompleteFinishKind, CompleteItem, CompleteOption, DurationCompleteItem, InsertMode, ISource, SortMethod } from './types' import { checkIgnoreRegexps, createKindMap, deltaCount, getInput, getResumeInput, MruLoader, shouldStop, toCompleteDoneItem } from './util' const logger = createLogger('completion') const TRIGGER_TIMEOUT = getConditionValue(200, 20) const CURSORMOVE_DEBOUNCE = getConditionValue(20, 20) export class Completion implements Disposable { public config: CompleteConfig private staticConfig: PopupMenuConfig private pum: PopupMenu private _mru: MruLoader private pretext: string | undefined private triggerTimer: NodeJS.Timeout private popupEvent: PopupChangeEvent private floating: Floating private disposables: Disposable[] = [] private complete: Complete | null = null private _debounced: ((bufnr: number, cursor: [number, number], hasInsert: boolean) => void) & { clear(): void } // Ordered items shown in the pum public activeItems: ReadonlyArray = [] private get nvim(): Neovim { return workspace.nvim } public init(): void { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) window.onDidChangeActiveTextEditor(e => { this.loadLocalConfig(e.document) }, null, this.disposables) this._mru = new MruLoader() this.pum = new PopupMenu(this.staticConfig, this._mru) this.floating = new Floating(this.staticConfig) this._debounced = debounce(this.onCursorMovedI.bind(this), CURSORMOVE_DEBOUNCE) events.on('BufEnter', () => { this._debounced.clear() }, null, this.disposables) events.on(['CursorMoved', 'InsertLeave'], () => { this.cancelAndClose() }, null, this.disposables) events.on('PumNavigate', () => { this.complete?.cancel() }, null, this.disposables) events.on('CursorMovedI', this._debounced, this, this.disposables) events.on('CursorMovedI', () => { clearTimeout(this.triggerTimer) }, null, this.disposables) events.on('InsertEnter', this.onInsertEnter, this, this.disposables) events.on('TextChangedI', this.onTextChangedI, this, this.disposables) events.on('MenuPopupChanged', async ev => { if (this.complete == null) return this.popupEvent = ev if (ev.inserted) this.complete.cancel() let selectedItem = this.activeItems[ev.index] let resolved = this.complete.resolveItem(selectedItem) if (!resolved || (!ev.move && this.complete.isCompleting)) return let detailRendered = selectedItem.detailRendered let showDocs = this.config.enableFloat await this.floating.resolveItem(resolved.source, resolved.item, this.option, showDocs, detailRendered) }, null, this.disposables) this.nvim.call('coc#ui#check_pum_keymappings', [this.config.autoTrigger], true) commands.registerCommand('editor.action.triggerSuggest', async (source?: string) => { await this.startCompletion({ source }) }, this, true) } public get mru(): MruLoader { return this._mru } public onCursorMovedI(bufnr: number, cursor: [number, number], hasInsert: boolean): void { if (hasInsert || !this.option || bufnr !== this.option.bufnr) return let { linenr, col } = this.option if (this.selectedItem && linenr === cursor[0] && col + byteLength(this.selectedItem.word) + 1 == cursor[1]) { return } // Possible cursor move out and move back, not cancel if (linenr === cursor[0] && cursor[1] === byteLength(toText(this.pretext)) + 1) { return } this.cancelAndClose() } public get option(): CompleteOption { if (!this.complete) return null return this.complete.option } public get isActivated(): boolean { return this.complete != null } public get selectedItem(): DurationCompleteItem | undefined { if (!this.popupEvent) return undefined return this.activeItems[this.popupEvent.index] } public get selectedCompletionInfo(): SelectedCompletionInfo | undefined { let item = this.selectedItem let { pretext } = this if (!pretext || !item) return undefined let line = this.option.linenr - 1 let end = pretext.length return { range: Range.create(line, item.character, line, end), text: item.word } } /** * Configuration for current document */ private loadLocalConfig(doc?: Document): void { let suggest = workspace.getConfiguration('suggest', doc) this.config = { autoTrigger: suggest.get('autoTrigger', 'always'), insertMode: suggest.get('insertMode', InsertMode.Replace), filterGraceful: suggest.get('filterGraceful', true), enableFloat: suggest.get('enableFloat', true), languageSourcePriority: suggest.get('languageSourcePriority', 99), snippetsSupport: suggest.get('snippetsSupport', true), defaultSortMethod: suggest.get('defaultSortMethod', SortMethod.Length), removeDuplicateItems: suggest.get('removeDuplicateItems', false), removeCurrentWord: suggest.get('removeCurrentWord', false), acceptSuggestionOnCommitCharacter: suggest.get('acceptSuggestionOnCommitCharacter', false), triggerCompletionWait: suggest.get('triggerCompletionWait', 0), triggerAfterInsertEnter: suggest.get('triggerAfterInsertEnter', false), maxItemCount: suggest.get('maxCompleteItemCount', 256), timeout: suggest.get('timeout', 500), minTriggerInputLength: suggest.get('minTriggerInputLength', 1), localityBonus: suggest.get('localityBonus', true), highPrioritySourceLimit: suggest.get('highPrioritySourceLimit', null), lowPrioritySourceLimit: suggest.get('lowPrioritySourceLimit', null), ignoreRegexps: suggest.get('ignoreRegexps', []), asciiMatch: suggest.get('asciiMatch', true), asciiCharactersOnly: suggest.get('asciiCharactersOnly', false), } } public loadConfiguration(e?: IConfigurationChangeEvent): CompleteConfig { if (e && !e.affectsConfiguration('suggest')) return if (e) this.pum.reset() let suggest = workspace.initialConfiguration.get('suggest') as any let labels = defaultValue(suggest.completionItemKindLabels, {}) this.staticConfig = Object.assign(this.staticConfig ?? {}, { kindMap: createKindMap(labels), defaultKindText: toText(labels['default']), detailField: suggest.detailField, detailMaxLength: toNumber(suggest.detailMaxLength, 100), invalidInsertCharacters: toArray(suggest.invalidInsertCharacters), formatItems: suggest.formatItems, filterOnBackspace: suggest.filterOnBackspace, floatConfig: toObject(suggest.floatConfig), pumFloatConfig: suggest.pumFloatConfig, labelMaxLength: suggest.labelMaxLength, reTriggerAfterIndent: !!suggest.reTriggerAfterIndent, reversePumAboveCursor: !!suggest.reversePumAboveCursor, snippetIndicator: toText(suggest.snippetIndicator), noselect: !!suggest.noselect, enablePreselect: !!suggest.enablePreselect, virtualText: !!suggest.virtualText, selection: suggest.selection }) let doc = workspace.getDocument(workspace.bufnr) this.loadLocalConfig(doc) } public async startCompletion(opt?: { source?: string, col?: number }): Promise { clearTimeout(this.triggerTimer) let sourceList: ISource[] if (Is.string(opt.source)) { sourceList = toArray(sources.getSource(opt.source)) } let doc = workspace.getAttachedDocument(events.bufnr) let info = await this.nvim.call('coc#util#change_info') as InsertChange info.pre = byteSlice(info.line, 0, info.col - 1) const option = this.getCompleteOption(doc, info, true, opt.col) await this._startCompletion(option, sourceList) } private async _startCompletion(option: CompleteOption, sourceList?: ISource[]): Promise { this._debounced.clear() let doc = workspace.getAttachedDocument(option.bufnr) option.filetype = doc.filetype logger.debug('trigger completion with', option) this.cancelAndClose() sourceList = sourceList ?? sources.getSources(option) if (isFalsyOrEmpty(sourceList)) return this.pretext = byteSlice(option.line, 0, option.colnr - 1) let complete = this.complete = new Complete( option, doc, this.config, sourceList) events.completing = true void events.fire('CompleteStart', [option]) complete.onDidRefresh(async () => { clearTimeout(this.triggerTimer) if (complete.isEmpty) { this.cancelAndClose(false) return } await this.filterResults() }) let shouldStop = await complete.doComplete() if (shouldStop) this.cancelAndClose(false) } public hasIndentChange(info: InsertChange): boolean { let { option, pretext } = this if (!option || option.linenr != info.lnum) return false let previous = pretext.match(/^\s*/)[0] let current = info.pre.match(/^\s*/)[0] if (previous == current || pretext.slice(previous.length) !== info.pre.slice(current.length)) return false return true } private async onTextChangedI(bufnr: number, info: InsertChange): Promise { const doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) return this._debounced.clear() const { option, staticConfig } = this const filterOnBackspace = this.staticConfig.filterOnBackspace if (option != null) { // detect item word insert if (!info.insertChar) { let pre = byteSlice(option.line, 0, option.col) if (this.selectedItem) { let { word, startcol } = this.popupEvent if (byteSlice(option.line, 0, startcol) + word == info.pre) { this.pretext = info.pre return } } else if (pre + this.pum.search == info.pre) { this.pretext = info.pre return } } // retrigger after indent if (staticConfig.reTriggerAfterIndent && this.hasIndentChange(info)) { this.cancelAndClose() await this.triggerCompletion(doc, info) return } if (shouldStop(bufnr, info, option) || (filterOnBackspace === false && info.pre.length < this.pretext.length)) { this.cancelAndClose() return } } if (info.pre === this.pretext) return clearTimeout(this.triggerTimer) let pretext = this.pretext = info.pre if (!info.insertChar) { if (this.complete) await this.filterResults() return } // check commit if (this.config.acceptSuggestionOnCommitCharacter && this.selectedItem) { let last = pretext.slice(-1) let resolvedItem = this.selectedItem let result = this.complete.resolveItem(resolvedItem) if (result && sources.shouldCommit(result.source, result.item, last)) { logger.debug(`commit by commit character: ${last}`) let startcol = byteIndex(this.option.line, resolvedItem.character) + 1 let delta = deltaCount(info) await this.nvim.call('coc#pum#replace', [startcol, resolvedItem.word, delta]) await this.stop(CompleteFinishKind.Confirm, true) let res = await this.nvim.evalVim(`[getline('.'),col('.'),mode()]`) as [string, number, string] let currentPre = byteSlice(res[0], 0, res[1] - 1) // Don't know how to decide feedkeys is Needed if (res[2] != 'i' || currentPre[currentPre.length - 1] == last) return if (pariedCharacters.has(last) && currentPre.slice(-2) == `${last}${pariedCharacters.get(last)}`) return // Need nvim_buf_set_text on vim9 to insert to end of line // if (info.line === pretext && last === ';') { this.nvim.call('feedkeys', [last, 'n'], true) return } } // trigger character if (!doc.chars.isKeywordChar(info.insertChar)) { let triggerSources = this.getTriggerSources(doc, pretext) if (triggerSources.length > 0) { await this.triggerCompletion(doc, info, triggerSources) return } } // trigger by normal character if (!this.complete) { await this.triggerCompletion(doc, info) return } if (this.complete.isEmpty) { // triggering without results this.triggerTimer = setTimeout(async () => { await this.triggerCompletion(doc, info) }, TRIGGER_TIMEOUT) return } await this.filterResults(info) } private getTriggerSources(doc: Document, pretext: string): ISource[] { let disabled = doc.getVar('disabled_sources', []) if (this.config.autoTrigger === 'none') return [] return sources.getTriggerSources(pretext, doc.filetype, doc.uri, disabled) } private async triggerCompletion(doc: Document, info: InsertChange, sources?: ISource[]): Promise { let { minTriggerInputLength, autoTrigger } = this.config let { pre } = info // check trigger if (autoTrigger === 'none') return false if (!sources && !this.shouldTrigger(doc, pre)) return false const option = this.getCompleteOption(doc, info) if (sources == null && option.input.length < minTriggerInputLength) { logger.trace(`Suggest not triggered with input "${option.input}", minimal trigger input length: ${minTriggerInputLength}`) return false } if (checkIgnoreRegexps(this.config.ignoreRegexps, option.input)) return false await this._startCompletion(option, sources) return true } private getCompleteOption(doc: Document, info: InsertChange, manual = false, col?: number): CompleteOption { let { pre } = info let input: string if (Is.number(col)) { input = byteSlice(info.line, col - 1, info.col - 1) } else { input = getInput(doc.chars, info.pre, this.config.asciiCharactersOnly) } let followWord = doc.getStartWord(info.line.slice(info.pre.length)) return { input, position: Position.create(info.lnum - 1, info.pre.length), line: info.line, followWord, filetype: doc.filetype, linenr: info.lnum, col: info.col - 1 - byteLength(input), colnr: info.col, bufnr: doc.bufnr, word: input + followWord, changedtick: info.changedtick, synname: '', filepath: doc.schema === 'file' ? URI.parse(doc.uri).fsPath : '', triggerCharacter: manual ? undefined : toText(pre[pre.length - 1]) } } public addMruItem(): void { let { selectedItem, complete } = this if (!selectedItem) return let character = selectedItem.character this._mru.add(complete.getTrigger(character), selectedItem) } public cancelAndClose(close = true): void { clearTimeout(this.triggerTimer) if (!this.complete) return const { linenr, bufnr } = this.complete.option this._onFinish(CompleteFinishKind.Normal, close) events.fire('CompleteDone', [{}, linenr, bufnr]).catch(onUnexpectedError) } private _onFinish(kind: CompleteFinishKind, close: boolean) { this.floating.cancel() let inserted = kind === CompleteFinishKind.Confirm || this.popupEvent?.inserted if (inserted) this.addMruItem() let doc = this.complete.document events.completing = false this.cancel() doc._forceSync() if (close) this.nvim.call('coc#pum#_close', [], true) } public async stop(kind: CompleteFinishKind, close = false): Promise { let { complete } = this if (complete == null) return const item = this.selectedItem const resolved = complete.resolveItem(item) const option = complete.option this._onFinish(kind, close) if (resolved && kind == CompleteFinishKind.Confirm) { await this.confirmCompletion(resolved.source, resolved.item, option) } events.fire('CompleteDone', [toCompleteDoneItem(item, resolved?.item), option.linenr, option.bufnr]).catch(onUnexpectedError) } private async confirmCompletion(source: ISource, item: CompleteItem, option: CompleteOption): Promise { await this.floating.resolveItem(source, item, option, false) if (!Is.func(source.onCompleteDone)) return let { insertMode, snippetsSupport } = this.config let opt: CompleteDoneOption = Object.assign({ insertMode, snippetsSupport }, option) await Promise.resolve(source.onCompleteDone(item, opt)) } private async onInsertEnter(bufnr: number): Promise { if (!this.config.triggerAfterInsertEnter || this.config.autoTrigger !== 'always') return let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) return let change = await this.nvim.call('coc#util#change_info') as InsertChange change.pre = byteSlice(change.line, 0, change.col - 1) await this.triggerCompletion(doc, change) } public shouldTrigger(doc: Document, pre: string): boolean { let { autoTrigger } = this.config if (autoTrigger == 'none') return false if (sources.shouldTrigger(pre, doc.filetype, doc.uri)) return true if (autoTrigger !== 'always') return false return true } public async filterResults(info?: InsertChange): Promise { let { complete, option, pretext } = this let search = getResumeInput(option, pretext) if (search == null || !complete) { this.cancelAndClose() return } let items = await complete.filterResults(search) // cancelled or have inserted text if (items === undefined || !this.option) return let doc = workspace.getDocument(option.bufnr) // trigger completion when trigger source available if (info && info.insertChar && items.length == 0) { let triggerSources = this.getTriggerSources(doc, pretext) if (triggerSources.length > 0) { await this.triggerCompletion(doc, info, triggerSources) return } } if (items.length == 0) { let last = search.slice(-1) if (!complete.isCompleting || last.length === 0 || !doc.chars.isKeywordChar(last)) { this.cancelAndClose() } return } this.activeItems = items this.pum.show(items, search, this.option) } public cancel(): void { if (this.complete != null) { this.complete.dispose() this.complete = null } if (this.triggerTimer != null) { clearTimeout(this.triggerTimer) this.triggerTimer = null } this.pretext = undefined this.activeItems = [] this.popupEvent = undefined } public dispose(): void { disposeAll(this.disposables) } } export default new Completion() ================================================ FILE: src/completion/keywords.ts ================================================ 'use strict' import { URI } from 'vscode-uri' import events from '../events' import { SyncItem } from '../model/bufferSync' import Document from '../model/document' import { DidChangeTextDocumentParams } from '../types' import { defaultValue } from '../util' import { forEach } from '../util/async' import { isGitIgnored } from '../util/fs' import { CancellationTokenSource } from '../util/protocol' export class KeywordsBuffer implements SyncItem { private lineWords: ReadonlyArray[] = [] private _gitIgnored = false private tokenSource: CancellationTokenSource | undefined private minimalCharacterLen = 2 constructor(private doc: Document, private segmenterLocales: string) { void this.parseWords(segmenterLocales) let uri = URI.parse(doc.uri) if (uri.scheme === 'file') { void isGitIgnored(uri.fsPath).then(ignored => { this._gitIgnored = ignored }) } } public getWords(): string[] { let res: string[] = [] for (let words of this.lineWords) { words.forEach(word => { if (!res.includes(word)) { res.push(word) } }) } return res } public cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = undefined } } public async parseWords(segmenterLocales: string | null): Promise { let { lineWords, doc, minimalCharacterLen } = this let { chars } = doc let tokenSource = this.tokenSource = new CancellationTokenSource() let token = tokenSource.token await forEach(doc.textDocument.lines, line => { let words = chars.matchLine(line, segmenterLocales, minimalCharacterLen) lineWords.push(words) }, token, { yieldAfter: 20 }) } public get bufnr(): number { return this.doc.bufnr } public get gitIgnored(): boolean { return this._gitIgnored } public onCompleteDone(idx: number): void { let { doc, segmenterLocales, minimalCharacterLen } = this let line = doc.getline(idx) this.lineWords[idx] = doc.chars.matchLine(line, segmenterLocales, minimalCharacterLen) } public onChange(e: DidChangeTextDocumentParams): void { if (events.completing || e.contentChanges.length == 0) return let { lineWords, doc, segmenterLocales, minimalCharacterLen } = this let { range, text } = e.contentChanges[0] let { start, end } = range let sl = start.line let el = end.line let del = el - sl let newLines = doc.textDocument.lines.slice(sl, sl + text.split(/\n/).length) let arr = newLines.map(line => doc.chars.matchLine(line, segmenterLocales, minimalCharacterLen)) lineWords.splice(sl, del + 1, ...arr) } public *matchWords(line: number): Iterable { let { lineWords } = this if (line >= lineWords.length) line = lineWords.length - 1 for (let i = 0; i < lineWords.length; i++) { let idx = i < line ? line - i - 1 : i let words = defaultValue(lineWords[idx], []) for (let word of words) { yield word } } } public dispose(): void { this.cancel() this.lineWords = [] } } ================================================ FILE: src/completion/match.ts ================================================ 'use strict' import { findIndex } from '../util/array' import { getCharCodes, wordChar, caseMatch } from '../util/fuzzy' import { getNextWord } from '../util/string' /** * Score and positions */ export type MatchResult = [number, ReadonlyArray] | undefined export function caseScore(input: number, curr: number, divide = 1): number { if (input === curr) return 1 / divide if (caseMatch(input, curr)) return 0.5 / divide return 0 } /** * Rules: * - First strict 5, first case match 2.5 * - First word character strict 2.5, first word character case 2 * - First fuzzy match strict 1, first fuzzy case 0.5 * - Follow strict 1, follow case 0.5 * - Follow word start 1, follow word case 0.75 * - First fuzzy strict 0.1, first fuzzy case 0.05 * @public * @param {string} word * @param {number[]} input * @returns {number} */ export function matchScore(word: string, input: Uint16Array): number { if (input.length == 0 || word.length < input.length) return 0 let next = nextScore(getCharCodes(word), 0, input, []) return next == null ? 0 : next[0] } export function matchScoreWithPositions(word: string, input: Uint16Array): [number, ReadonlyArray] | undefined { if (input.length == 0 || word.length < input.length) return undefined return nextScore(getCharCodes(word), 0, input, []) } /** * Return score and positions. */ function nextScore(codes: Uint16Array, index: number, inputCodes: Uint16Array, positions: ReadonlyArray, inputIndex = 0): MatchResult { let input = inputCodes[inputIndex] if (input === undefined) return [0, positions] let len = codes.length // let nextCodes = inputCodes.slice(1) let nextIndex = inputIndex + 1 // not alphabet if (!wordChar(input)) { let idx = findIndex(codes, input, index) if (idx == -1) return undefined let score = idx == 0 ? 5 : 1 let next = nextScore(codes, idx + 1, inputCodes, [...positions, idx], nextIndex) return next === undefined ? undefined : [score + next[0], next[1]] } // check beginning let isStart = index === 0 let score = caseScore(input, codes[index], isStart ? 0.2 : 1) if (score > 0) { let next = nextScore(codes, index + 1, inputCodes, [...positions, index], nextIndex) return next === undefined ? undefined : [score + next[0], next[1]] } // check next word let positionMap: Map> = new Map() let word = getNextWord(codes, index + 1) if (word != null) { let score = caseScore(input, word[1], isStart ? 0.5 : 1) if (score > 0) { let ps = [...positions, word[0]] if (score === 0.5) score = 0.75 let next = nextScore(codes, word[0] + 1, inputCodes, ps, nextIndex) if (next !== undefined) positionMap.set(score + next[0], next[1]) } } // find fuzzy for (let i = index + 1; i < len; i++) { let score = caseScore(input, codes[i], isStart ? 1 : 10) if (score > 0) { let next = nextScore(codes, i + 1, inputCodes, [...positions, i], nextIndex) if (next !== undefined) positionMap.set(score + next[0], next[1]) break } } if (positionMap.size == 0) { // Try match previous position if (positions.length > 0) { let last = positions[positions.length - 1] if (last > 0 && codes[last] !== input && codes[last - 1] === input) { let ps = positions.slice() ps.splice(positions.length - 1, 0, last - 1) let next = nextScore(codes, last + 1, inputCodes, ps, nextIndex) if (next === undefined) return undefined return [0.5 + next[0], next[1]] } } return undefined } let max = Math.max(...positionMap.keys()) return [max, positionMap.get(max)] } ================================================ FILE: src/completion/native/around.ts ================================================ 'use strict' import BufferSync from '../../model/bufferSync' import { waitImmediate } from '../../util' import { CancellationToken } from '../../util/protocol' import { KeywordsBuffer } from '../keywords' import Source from '../source' import { CompleteOption, CompleteResult, ExtendedCompleteItem, ISource } from '../types' export class Around extends Source { constructor(private keywords: BufferSync) { super({ name: 'around', filepath: __filename }) } public async doComplete(opt: CompleteOption, token: CancellationToken): Promise> { const shouldRun = await this.checkComplete(opt) if (!shouldRun) return null let { bufnr, input, word, linenr, triggerForInComplete } = opt if (input.length === 0) return null await waitImmediate() let buf = this.keywords.getItem(bufnr) if (!buf) return null if (!triggerForInComplete) this.noMatchWords = new Set() if (token.isCancellationRequested) return null let iterable = buf.matchWords(linenr - 1) let items: Set = new Set() let isIncomplete = await this.getResults([iterable], input, word, items, token) return { isIncomplete, items: Array.from(items, word => ({ word })) } } } export function register(sourceMap: Map, keywords: BufferSync): void { let source = new Around(keywords) sourceMap.set('around', source) } ================================================ FILE: src/completion/native/buffer.ts ================================================ 'use strict' import BufferSync from '../../model/bufferSync' import { waitImmediate } from '../../util' import { CancellationToken } from '../../util/protocol' import { KeywordsBuffer } from '../keywords' import Source from '../source' import { CompleteOption, CompleteResult, ExtendedCompleteItem, ISource } from '../types' export class Buffer extends Source { constructor(private keywords: BufferSync) { super({ name: 'buffer', filepath: __filename }) } public get ignoreGitignore(): boolean { return this.getConfig('ignoreGitignore', true) } public async doComplete(opt: CompleteOption, token: CancellationToken): Promise> { const shouldRun = await this.checkComplete(opt) if (!shouldRun) return null let { bufnr, input, word, triggerForInComplete } = opt if (input.length === 0) return null await waitImmediate() if (!triggerForInComplete) this.noMatchWords = new Set() if (token.isCancellationRequested) return null let iterables: Iterable[] = [] for (let buf of this.keywords.items) { if (buf.bufnr === bufnr || (this.ignoreGitignore && buf.gitIgnored)) continue iterables.push(buf.matchWords(0)) } let items: Set = new Set() let isIncomplete = await this.getResults(iterables, input, word, items, token) return { isIncomplete, items: Array.from(items).map(s => { return { word: s } }) } } } export function register(sourceMap: Map, keywords: BufferSync): void { let source = new Buffer(keywords) sourceMap.set('buffer', source) } ================================================ FILE: src/completion/native/file.ts ================================================ 'use strict' import { CompletionItemKind } from 'vscode-languageserver-types' import { statAsync } from '../../util/fs' import { fs, minimatch, path, promisify } from '../../util/node' import { isWindows } from '../../util/platform' import { CancellationToken } from '../../util/protocol' import { byteSlice } from '../../util/string' import workspace from '../../workspace' import Source from '../source' import { CompleteOption, CompleteResult, ExtendedCompleteItem, ISource, VimCompleteItem } from '../types' const pathRe = /(?:\.{0,2}|~|\$HOME|([\w]+)|[a-zA-Z]:|)(\/|\\+)(?:[\u4E00-\u9FA5\u00A0-\u024F\w .@()-]+(\/|\\+))*(?:[\u4E00-\u9FA5\u00A0-\u024F\w .@()-])*$/ const invalid_characters = new Set(['#', '<', '$', '+', '%', '>', '!', '`', '&', '*', "'", '|', '{', '?', '"', '=', '}', '@']) interface PathOption { pathstr: string part: string startcol: number input: string } export function resolveEnvVariables(str: string, env = process.env): string { let replaced = str // windows replaced = replaced.replace(/%([^%]+)%/g, (m, n) => env[n] ?? m) // linux and mac replaced = replaced.replace( /\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/gi, (m, a, b) => (env[a || b] ?? m) ) return replaced } export function getLastPart(text: string): string | null { let begin = text.length - 1 while (begin >= 0) { let curr = text[begin] if (invalid_characters.has(curr)) { begin++ break } if (curr == ' ' && text[begin - 1] !== '\\') { begin++ break } if (begin == 0) break begin-- } if (begin >= text.length) return null return text.slice(begin) } export async function getFileItem(root: string, filename: string): Promise { let f = path.join(root, filename) let stat = await statAsync(f) if (stat) { let dir = stat.isDirectory() let abbr = dir ? filename + '/' : filename let word = filename return { word, abbr, kind: dir ? CompletionItemKind.Folder : CompletionItemKind.File } } return null } export function filterFiles(files: string[], ignoreHidden: boolean, ignorePatterns: string[] = []): string[] { return files.filter(f => { if (!f || (ignoreHidden && f.startsWith("."))) return false for (let p of ignorePatterns) { if (minimatch(f, p, { dot: true })) return false } return true }) } export function getDirectory(pathstr: string, root: string): string { let part = /[\\/]$/.test(pathstr) ? pathstr : path.dirname(pathstr) return path.isAbsolute(pathstr) ? part : path.join(root, part) } export async function getItemsFromRoot(pathstr: string, root: string, ignoreHidden: boolean, ignorePatterns: string[]): Promise { let res = [] let dir = getDirectory(pathstr, root) let stat = await statAsync(dir) if (stat && stat.isDirectory()) { let files = await promisify(fs.readdir)(dir) files = filterFiles(files, ignoreHidden, ignorePatterns) let items = await Promise.all(files.map(filename => getFileItem(dir, filename))) res = res.concat(items) } res = res.filter(item => item != null) return res } export class File extends Source { constructor(public isWindows: boolean) { super({ name: 'file', filepath: __filename }) } public get triggerCharacters(): string[] { let characters = this.getConfig('triggerCharacters', []) return this.isWindows ? characters : characters.filter(s => s != '\\') } private getPathOption(opt: CompleteOption): PathOption | null { let { line, colnr } = opt let part = resolveEnvVariables(byteSlice(line, 0, colnr - 1)) let filepath = getLastPart(part) if (!filepath || filepath.endsWith('//')) return null let ms = part.match(pathRe) if (ms && ms.length) { const pathstr = workspace.expand(ms[0]) let input = ms[0].match(/[^/\\]*$/)[0] return { pathstr, part: ms[1], startcol: colnr - input.length - 1, input } } return null } public shouldTrim(ext: string): boolean { let trimSameExts = this.getConfig('trimSameExts', []) return trimSameExts.includes(ext) } public async getRoot(pathstr: string, part: string, filepath: string, cwd: string): Promise { let root: string | undefined let dirname = filepath ? path.dirname(filepath) : '' if (pathstr.startsWith(".")) { root = filepath ? dirname : cwd } else if (this.isWindows && /^\w+:/.test(pathstr)) { root = /[\\/]$/.test(pathstr) ? pathstr : path.win32.dirname(pathstr) } else if (!this.isWindows && pathstr.startsWith("/")) { root = pathstr.endsWith("/") ? pathstr : path.posix.dirname(pathstr) } else if (part) { let exists = await promisify(fs.exists)(path.join(dirname, part)) if (exists) { root = dirname } else { exists = await promisify(fs.exists)(path.join(cwd, part)) if (exists) root = cwd } } else { root = cwd } return root } public async doComplete(opt: CompleteOption, token: CancellationToken): Promise> { const shouldRun = await this.checkComplete(opt) if (!shouldRun) return null let { filepath } = opt let option = this.getPathOption(opt) if (!option || option.startcol < opt.col) return null let { pathstr, part, startcol } = option let startPart = opt.col == startcol ? '' : byteSlice(opt.line, opt.col, startcol) let ext = path.extname(path.basename(filepath)) let cwd = await this.nvim.call('getcwd', []) as string let root = await this.getRoot(pathstr, part, filepath, cwd) if (!root || token.isCancellationRequested) return null let items = await getItemsFromRoot(pathstr, root, this.getConfig('ignoreHidden', true), this.getConfig('ignorePatterns', [])) let trimExt = this.shouldTrim(ext) return { items: items.map(item => { let ex = path.extname(item.word) item.word = trimExt && ex === ext ? item.word.replace(ext, '') : item.word return { word: `${startPart}${item.word}`, abbr: `${startPart}${item.abbr}`, menu: this.menu } }) } } } export function register(sourceMap: Map): void { sourceMap.set('file', new File(isWindows)) } ================================================ FILE: src/completion/pum.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { CompletionItemKind } from 'vscode-languageserver-types' import { matchSpansReverse } from '../model/fuzzyMatch' import { FloatConfig, HighlightItem } from '../types' import { isFalsyOrEmpty } from '../util/array' import { anyScore } from '../util/filter' import * as Is from '../util/is' import { toNumber } from '../util/numbers' import { byteIndex, byteLength, characterIndex, toText } from '../util/string' import workspace from '../workspace' import { CompleteOption, DurationCompleteItem } from './types' import { getKindHighlight, getKindText, highlightOffset, MruLoader, Selection } from './util' export interface PumDimension { readonly height: number readonly width: number readonly row: number readonly col: number readonly scrollbar: boolean } // 0 based col start & end export interface HighlightRange { start: number end: number hlGroup: string } export interface LabelWithDetail { text: string highlights: HighlightRange[] } export interface BuildConfig { border: boolean abbrWidth: number menuWidth: number kindWidth: number shortcutWidth: number } export interface PumConfig { width?: number highlights?: HighlightItem[] highlight?: string borderhighlight?: string title?: string winblend?: number shadow?: boolean border?: [number, number, number, number] | undefined rounded?: number reverse?: boolean } export interface PopupMenuConfig { kindMap: Map defaultKindText: string noselect: boolean selection: Selection enablePreselect: boolean filterOnBackspace: boolean floatConfig: FloatConfig pumFloatConfig?: FloatConfig formatItems: ReadonlyArray labelMaxLength: number reversePumAboveCursor: boolean snippetIndicator: string virtualText: boolean detailMaxLength: number detailField: string reTriggerAfterIndent: boolean invalidInsertCharacters: string[] } export enum HighlightGroups { PumDetail = 'CocPumDetail', PumDeprecated = 'CocPumDeprecated', PumMenu = 'CocPumMenu', PumShortcut = 'CocPumShortcut', PumSearch = 'CocPumSearch', } export enum PumItems { Abbr = 'abbr', Menu = 'menu', Kind = 'kind', Shortcut = 'shortcut' } export default class PopupMenu { private _search = '' private _pumConfig: PumConfig constructor( private config: PopupMenuConfig, private mruLoader: MruLoader ) { } private get nvim(): Neovim { return workspace.nvim } public get search(): string { return this._search } public reset(): void { this._search = '' this._pumConfig = undefined } public get pumConfig(): PumConfig { if (this._pumConfig) return this._pumConfig let { floatConfig, pumFloatConfig, reversePumAboveCursor } = this.config if (!pumFloatConfig) pumFloatConfig = floatConfig let obj: PumConfig = {} if (pumFloatConfig.border) { obj.border = [1, 1, 1, 1] obj.rounded = pumFloatConfig.rounded ? 1 : 0 obj.borderhighlight = pumFloatConfig.borderhighlight ?? 'CocFloatBorder' } if (Is.string(pumFloatConfig.highlight)) obj.highlight = pumFloatConfig.highlight if (Is.number(pumFloatConfig.winblend)) obj.winblend = pumFloatConfig.winblend if (Is.string(pumFloatConfig.title)) obj.title = pumFloatConfig.title obj.shadow = pumFloatConfig.shadow === true obj.reverse = reversePumAboveCursor === true this._pumConfig = obj return obj } private stringWidth(text: string, cache = false): number { return workspace.getDisplayWidth(text, cache) } public show(items: DurationCompleteItem[], search: string, option: CompleteOption): void { this._search = search let { noselect, enablePreselect, invalidInsertCharacters, selection, virtualText, kindMap, defaultKindText } = this.config const invalidInsertCodes = invalidInsertCharacters.map(ch => ch.charCodeAt(0)) let selectedIndex = enablePreselect ? items.findIndex(o => o.preselect) : -1 let maxMru = -1 let abbrWidth = 0 let menuWidth = 0 let kindWidth = 0 let shortcutWidth = 0 let checkMru = selectedIndex == -1 && !noselect && selection !== Selection.First let labels: LabelWithDetail[] = [] let baseCharacter = characterIndex(option.line, option.col) let minCharacter = baseCharacter // abbr kind, menu for (let i = 0; i < items.length; i++) { let item = items[i] if (checkMru) { let n = this.mruLoader.getScore(search, item, selection) if (n > maxMru) { maxMru = n selectedIndex = i } } if (Is.number(item.character) && item.character < minCharacter) { minCharacter = item.character } let label = this.getLabel(item) labels.push(label) abbrWidth = Math.max(this.stringWidth(label.text, true), abbrWidth) if (item.kind) kindWidth = Math.max(this.stringWidth(getKindText(item.kind, kindMap, defaultKindText), true), kindWidth) if (item.menu) menuWidth = Math.max(this.stringWidth(item.menu, true), menuWidth) if (item.shortcut) shortcutWidth = Math.max(this.stringWidth(item.shortcut, true) + 2, shortcutWidth) } if (selectedIndex !== -1 && search.length > 0) { let item = items[selectedIndex] if (!item.word.startsWith(search) && !item.filterText.startsWith(search)) { selectedIndex = -1 } } if (!noselect) { selectedIndex = selectedIndex == -1 ? 0 : selectedIndex } else { if (selectedIndex > 0) { let [item] = items.splice(selectedIndex, 1) items.unshift(item) let [label] = labels.splice(selectedIndex, 1) labels.unshift(label) } selectedIndex = -1 } let opt = { input: search, index: selectedIndex, bufnr: option.bufnr, line: option.linenr, // col for pum col: option.col, // col for word insert startcol: byteIndex(option.line, minCharacter), virtualText, words: items.map(o => { let character = o.character let start = Math.max(1, option.position.character - character + 1) let word = getInsertWord(o.word, invalidInsertCodes, start) return prefixWord(word, character, option.line, minCharacter) }) } let pumConfig = this.pumConfig let lines: string[] = [] let highlights: HighlightItem[] = [] // create lines and highlights let width = 0 let buildConfig: BuildConfig = { border: !!pumConfig.border, menuWidth, abbrWidth, kindWidth, shortcutWidth } this.adjustAbbrWidth(buildConfig) let lowInput = search.toLowerCase() for (let index = 0; index < items.length; index++) { let [displayWidth, text] = this.buildItem(search, lowInput, items[index], labels[index], highlights, index, buildConfig) width = Math.max(width, displayWidth) lines.push(text) } let config: PumConfig = Object.assign({ width, highlights }, pumConfig) this.nvim.call('coc#pum#create', [lines, opt, config], true) this.nvim.redrawVim() } private getLabel(item: DurationCompleteItem): LabelWithDetail { let { labelDetails, detail } = item let { snippetIndicator, labelMaxLength, detailField, detailMaxLength } = this.config let label = item.abbr! let hls: HighlightRange[] = [] if (item.isSnippet && !label.endsWith(snippetIndicator)) { label = label + snippetIndicator } if (detailField === 'abbr' && detail && !labelDetails && detail.length < detailMaxLength) { labelDetails = { detail: ' ' + detail.replace(/\r?\n\s*/g, ' ') } } if (labelDetails) { let added = (labelDetails.detail ?? '') + (labelDetails.description ? ` ${labelDetails.description}` : '') if (label.length + added.length <= labelMaxLength) { let start = byteLength(label) hls.push({ start, end: start + byteLength(added), hlGroup: HighlightGroups.PumDetail }) label = label + added item.detailRendered = true } } if (label.length > labelMaxLength) { label = label.slice(0, labelMaxLength - 1) + '.' } return { text: label, highlights: hls } } private adjustAbbrWidth(config: BuildConfig): void { let { formatItems } = this.config let pumwidth = toNumber(workspace.env.pumwidth, 15) let len = 0 for (const item of formatItems) { if (item == PumItems.Abbr) { len += config.abbrWidth + 1 } else if (item == PumItems.Menu && config.menuWidth) { len += config.menuWidth + 1 } else if (item == PumItems.Kind && config.kindWidth) { len += config.kindWidth + 1 } else if (item == PumItems.Shortcut && config.shortcutWidth) { len += config.shortcutWidth + 1 } } if (len < pumwidth) { config.abbrWidth = config.abbrWidth + pumwidth - len } } private buildItem(input: string, lowInput: string, item: DurationCompleteItem, label: LabelWithDetail, hls: HighlightItem[], index: number, config: BuildConfig): [number, string] { // abbr menu kind shortcut let { labelMaxLength, formatItems, kindMap, defaultKindText } = this.config let text = config.border ? '' : ' ' let len = byteLength(text) let displayWidth = text.length let append = (str: string, width: number): void => { let s = this.fillWidth(str, width) displayWidth += width len += byteLength(s) text += s } for (const name of formatItems) { switch (name) { case 'abbr': { if (!isFalsyOrEmpty(item.positions)) { let pre = highlightOffset(len, item) if (pre != -1) { positionHighlights(hls, item.filterText, item.positions, pre, index, labelMaxLength) } else { let score = anyScore(input, lowInput, 0, item.abbr, item.abbr.toLowerCase(), 0) positionHighlights(hls, item.abbr, score, len, index, labelMaxLength) } } let abbr = label.text let start = len append(abbr, config.abbrWidth + 1) label.highlights.forEach(hl => { hls.push({ hlGroup: hl.hlGroup, lnum: index, colStart: start + hl.start, colEnd: start + hl.end }) }) if (item.deprecated) { hls.push({ hlGroup: HighlightGroups.PumDeprecated, lnum: index, colStart: start, colEnd: len - 1, }) } break } case 'menu': { if (config.menuWidth > 0) { let colStart = len append(toText(item.menu), config.menuWidth + 1) if (item.menu) { hls.push({ hlGroup: HighlightGroups.PumMenu, lnum: index, colStart, colEnd: colStart + byteLength(item.menu) }) } } break } case 'kind': if (config.kindWidth > 0) { let { kind } = item let kindText = getKindText(kind, kindMap, defaultKindText) let colStart = len append(toText(kindText), config.kindWidth + 1) if (kindText) { hls.push({ hlGroup: getKindHighlight(kind), lnum: index, colStart, colEnd: colStart + byteLength(kindText) }) } } break case 'shortcut': if (config.shortcutWidth > 0) { let colStart = len let shortcut = item.shortcut append(shortcut ? `[${shortcut}]` : '', config.shortcutWidth + 1) if (shortcut) { hls.push({ hlGroup: HighlightGroups.PumShortcut, lnum: index, colStart, colEnd: colStart + byteLength(shortcut) + 2 }) } } break } } return [displayWidth, text] } public fillWidth(text: string, width: number): string { let n = width - this.stringWidth(text) return text + ' '.repeat(Math.max(n, 0)) } } /** * positions is FuzzyScore */ function positionHighlights(hls: HighlightItem[], label: string, positions: ArrayLike, pre: number, line: number, max: number): void { for (let span of matchSpansReverse(label, positions, 2, max)) { hls.push({ hlGroup: HighlightGroups.PumSearch, lnum: line, colStart: pre + span[0], colEnd: pre + span[1], }) } } /** * Exclude part with invalid characters. */ export function getInsertWord(word: string, codes: number[], start: number): string { if (codes.length === 0) return word for (let i = start; i < word.length; i++) { if (codes.includes(word.charCodeAt(i))) { return word.slice(0, i) } } return word } /** * Append previous text to word when necessary */ export function prefixWord(word: string, character: number, line: string, minCharacter: number): string { return minCharacter < character ? line.slice(minCharacter, character) + word : word } ================================================ FILE: src/completion/source-language.ts ================================================ 'use strict' import { CompletionItem, InsertReplaceEdit, Range, TextEdit } from 'vscode-languageserver-types' import commands from '../commands' import { getLineAndPosition } from '../core/ui' import { createLogger } from '../logger' import Document from '../model/document' import { CompletionItemProvider, DocumentSelector } from '../provider' import snippetManager from '../snippets/manager' import { UltiSnippetOption } from '../types' import { pariedCharacters, waitImmediate } from '../util' import { isFalsyOrEmpty, toArray } from '../util/array' import { CancellationError } from '../util/errors' import * as Is from '../util/is' import { toObject } from '../util/object' import { CancellationToken, CompletionTriggerKind } from '../util/protocol' import { characterIndex } from '../util/string' import workspace from '../workspace' import { CompleteDoneOption, CompleteOption, CompleteResult, InsertMode, ISource, ItemDefaults, SourceType } from './types' import { getReplaceRange, isSnippetItem } from './util' const logger = createLogger('source-language') interface TriggerContext { line: string lnum: number character: number } export default class LanguageSource implements ISource { public readonly sourceType = SourceType.Service private _enabled = true private itemDefaults: ItemDefaults = {} private hasDefaultRange: boolean // cursor position on trigger private triggerContext: TriggerContext | undefined // Kept Promise for resolve private resolving: WeakMap> = new WeakMap() constructor( public readonly name: string, public readonly shortcut: string, private provider: CompletionItemProvider, public readonly documentSelector: DocumentSelector, public readonly triggerCharacters: string[], public readonly allCommitCharacters: string[], public readonly priority: number | undefined ) { } public get enable(): boolean { return this._enabled } public toggle(): void { this._enabled = !this._enabled } public shouldCommit(item: CompletionItem, character: string): boolean { if (this.allCommitCharacters.includes(character)) return true let commitCharacters = toArray(item.commitCharacters ?? this.itemDefaults.commitCharacters) return commitCharacters.includes(character) } public async doComplete(option: CompleteOption, token: CancellationToken): Promise | null> { let { triggerCharacter, bufnr, position } = option let triggerKind: CompletionTriggerKind = this.getTriggerKind(option) this.triggerContext = { lnum: position.line, character: position.character, line: option.line } let context: any = { triggerKind, option } if (triggerKind == CompletionTriggerKind.TriggerCharacter) context.triggerCharacter = triggerCharacter let textDocument = workspace.getDocument(bufnr).textDocument await waitImmediate() let result = await Promise.resolve(this.provider.provideCompletionItems(textDocument, position, token, context)) if (!result || token.isCancellationRequested) return null let completeItems = Array.isArray(result) ? result : result.items if (!completeItems || completeItems.length == 0) return null let itemDefaults = this.itemDefaults = toObject(result['itemDefaults']) let isIncomplete = Is.isCompletionList(result) ? result.isIncomplete === true : false this.hasDefaultRange = Is.isEditRange(itemDefaults.editRange) return { isIncomplete, items: completeItems, itemDefaults } } public onCompleteResolve(item: CompletionItem, opt: CompleteOption | undefined, token: CancellationToken): Promise | void { let hasResolve = Is.func(this.provider.resolveCompletionItem) if (!hasResolve) return let promise = this.resolving.get(item) if (promise) return promise let invalid = false promise = new Promise(async (resolve, reject) => { let disposable = token.onCancellationRequested(() => { this.resolving.delete(item) reject(new CancellationError()) }) try { let resolved = await Promise.resolve(this.provider.resolveCompletionItem(item, token)) disposable.dispose() if (!token.isCancellationRequested) { if (!resolved) { invalid = true this.resolving.delete(item) } else { if (resolved.textEdit) { let character = characterIndex(opt.line, opt.col) resolved.textEdit = fixTextEdit(character, resolved.textEdit) } // addDocumentation(item, completeItem, opt.filetype) Object.assign(item, resolved) } } resolve() } catch (e) { invalid = true this.resolving.delete(item) // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject(e) } }) if (!invalid) { this.resolving.set(item, promise) } return promise } public async onCompleteDone(item: CompletionItem, opt: CompleteDoneOption): Promise { let doc = workspace.getDocument(opt.bufnr) await doc.patchChange() let additionalEdits = !isFalsyOrEmpty(item.additionalTextEdits) let version = doc.version let isSnippet = await this.applyTextEdit(doc, additionalEdits, item, opt) if (additionalEdits) { // move cursor after edit await doc.applyEdits(item.additionalTextEdits, doc.version != version, !isSnippet) if (isSnippet) await snippetManager.selectCurrentPlaceholder() } if (item.command) { if (commands.has(item.command.command)) { void commands.execute(item.command) } else { logger.warn(`Command "${item.command.command}" not registered to coc.nvim`) } } } private async applyTextEdit(doc: Document, additionalEdits: boolean, item: CompletionItem, option: CompleteDoneOption): Promise { let { linenr, col } = option let { character, line } = this.triggerContext let pos = await getLineAndPosition(workspace.nvim) if (pos.line != linenr - 1) return let { textEdit, textEditText, insertText, label } = item let range = getReplaceRange(item, this.itemDefaults?.editRange, undefined, option.insertMode) if (!range) { // create default replace range let end = character + (option.insertMode == InsertMode.Insert ? 0 : option.followWord.length) range = Range.create(pos.line, characterIndex(line, col), pos.line, end) } // replace range must contains cursor position. let invalidRangeEnd = range.end.character < character if (invalidRangeEnd) range.end.character = character let newText = textEdit ? textEdit.newText : (textEditText && this.hasDefaultRange ? textEditText : insertText) ?? label // adjust range by indent let indentCount = fixIndent(line, pos.text, range) // cursor moved count let delta = pos.character - character - indentCount // fix range by count cursor moved to replace insert word on complete done. if (delta !== 0) range.end.character += delta let next = pos.text[range.end.character] if (invalidRangeEnd && next && newText.endsWith(next) && pariedCharacters.get(newText[0]) === next) { range.end.character += 1 } if (option.snippetsSupport !== false && isSnippetItem(item, this.itemDefaults)) { let opts = getUltisnipOption(item) let insertTextMode = item.insertTextMode ?? this.itemDefaults.insertTextMode return await snippetManager.insertSnippet(newText, !additionalEdits, range, insertTextMode, opts) } await doc.applyEdits([TextEdit.replace(range, newText)], false, pos) return false } private getTriggerKind(opt: CompleteOption): CompletionTriggerKind { let { triggerCharacters } = this let isTrigger = triggerCharacters.includes(opt.triggerCharacter) let triggerKind: CompletionTriggerKind = CompletionTriggerKind.Invoked if (opt.triggerForInComplete) { triggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions } else if (isTrigger) { triggerKind = CompletionTriggerKind.TriggerCharacter } return triggerKind } } export function getUltisnipOption(item: CompletionItem): UltiSnippetOption | undefined { let opts = item.data?.ultisnip === true ? {} : item.data?.ultisnip return opts ? opts : undefined } export function fixIndent(line: string, currline: string, range: Range): number { let oldIndent = line.match(/^\s*/)[0] let newIndent = currline.match(/^\s*/)[0] if (oldIndent === newIndent) return 0 let d = newIndent.length - oldIndent.length range.start.character += d range.end.character += d return d } export function fixTextEdit(character: number, edit: TextEdit | InsertReplaceEdit): TextEdit | InsertReplaceEdit { if (TextEdit.is(edit)) { if (character < edit.range.start.character) { edit.range.start.character = character } } if (InsertReplaceEdit.is(edit)) { if (character < edit.insert.start.character) { edit.insert.start.character = character } if (character < edit.replace.start.character) { edit.replace.start.character = character } } return edit } ================================================ FILE: src/completion/source-vim.ts ================================================ 'use strict' import { Range } from 'vscode-languageserver-types' import { getLineAndPosition } from '../core/ui' import snippetManager from '../snippets/manager' import { CancellationToken } from '../util/protocol' import { byteSlice, characterIndex } from '../util/string' import workspace from '../workspace' import Source from './source' import * as Is from '../util/is' import { CompleteOption, CompleteResult, ExtendedCompleteItem } from './types' export function getMethodName(name: string, names: ReadonlyArray): string | undefined { if (names.includes(name)) return name let key = name[0].toUpperCase() + name.slice(1) if (names.includes(key)) return key throw new Error(`${name} not exists`) } export function checkInclude(name: string, fns: ReadonlyArray): boolean { if (fns.includes(name)) return true let key = name[0].toUpperCase() + name.slice(1) return fns.includes(key) } export default class VimSource extends Source { private async callOptionalFunc(fname: string, args: any[], isNotify = false): Promise { let exists = checkInclude(fname, this.remoteFns) if (!exists) return null let name = `coc#source#${this.name}#${getMethodName(fname, this.remoteFns)}` if (isNotify) return this.nvim.call(name, args, true) return await this.nvim.call(name, args) } public async checkComplete(opt: CompleteOption): Promise { let shouldRun = await super.checkComplete(opt) if (!shouldRun) return false if (!checkInclude('should_complete', this.remoteFns)) return true let res = await this.callOptionalFunc('should_complete', [opt]) return !!res } public async refresh(): Promise { await this.callOptionalFunc('refresh', []) } public async insertSnippet(insertText: string, opt: CompleteOption): Promise { let pos = await getLineAndPosition(this.nvim) let { line, col } = opt let oldIndent = line.match(/^\s*/)[0] let newIndent = pos.text.match(/^\s*/)[0] // current insert range let range = Range.create(pos.line, characterIndex(line, col) + newIndent.length - oldIndent.length, pos.line, pos.character) await snippetManager.insertSnippet(insertText, true, range) } public async onCompleteDone(item: ExtendedCompleteItem, opt: CompleteOption): Promise { if (checkInclude('on_complete', this.remoteFns)) { await this.callOptionalFunc('on_complete', [item], true) } else if (item.isSnippet && item.insertText) { await this.insertSnippet(item.insertText, opt) } } public onEnter(bufnr: number): void { let doc = workspace.getDocument(bufnr) if (!doc || !checkInclude('on_enter', this.remoteFns)) return let { filetypes } = this if (filetypes && !filetypes.includes(doc.filetype)) return void this.callOptionalFunc('on_enter', [{ bufnr, uri: doc.uri, languageId: doc.filetype }], true) } public async doComplete(opt: CompleteOption, token: CancellationToken): Promise | null> { let shouldRun = await this.checkComplete(opt) if (!shouldRun) return null let startcol: number | undefined = await this.callOptionalFunc('get_startcol', [opt]) if (token.isCancellationRequested) return null let { col, input, line, colnr } = opt if (Is.number(startcol) && startcol >= 0 && startcol !== col) { input = byteSlice(line, startcol, colnr - 1) opt = Object.assign({}, opt, { col: startcol, changed: col - startcol, input }) } const vim9 = this.remoteFns.includes('Complete') let vimItems = await this.nvim.callAsync('coc#_do_complete', [this.name, { ...opt, vim9 }]) as (ExtendedCompleteItem | string)[] if (!vimItems || vimItems.length == 0 || token.isCancellationRequested) return null let checkFirst = this.firstMatch && input.length > 0 let inputFirst = checkFirst ? input[0].toLowerCase() : '' let items: ExtendedCompleteItem[] = [] vimItems.forEach(item => { let obj: ExtendedCompleteItem = Is.string(item) ? { word: item } : item if (checkFirst) { let ch = (obj.filterText ?? obj.word)[0] if (inputFirst && ch.toLowerCase() !== inputFirst) return } if (this.isSnippet) obj.isSnippet = true items.push(obj) }) return { items, startcol: Is.number(startcol) ? startcol : undefined } } } ================================================ FILE: src/completion/source.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import { DocumentSelector } from '../provider' import { defaultValue, disposeAll, getConditionValue, waitImmediate } from '../util' import { isFalsyOrEmpty, toArray } from '../util/array' import { ASCII_END } from '../util/constants' import { caseMatch, fuzzyMatch, getCharCodes } from '../util/fuzzy' import * as Is from '../util/is' import { unidecode } from '../util/node' import { CancellationToken, Disposable } from '../util/protocol' import { isAlphabet } from '../util/string' import workspace from '../workspace' import { CompleteOption, CompleteResult, ExtendedCompleteItem, ISource, SourceConfig, SourceType } from './types' const WORD_PREFIXES = ['_', '$', '-'] const WORD_PREFIXES_CODE = [95, 36, 45] const MAX_DURATION = getConditionValue(80, 20) const MAX_COUNT = 50 export interface SourceConfiguration { readonly priority?: number readonly triggerCharacters?: string[] readonly firstMatch?: boolean readonly triggerPatterns?: string[] readonly shortcut?: string readonly enable?: boolean readonly filetypes?: string[] readonly disableSyntaxes?: string[] } export default class Source implements ISource { public readonly name: string public readonly filepath: string public readonly sourceType: SourceType public readonly isSnippet: boolean public readonly documentSelector: DocumentSelector | undefined /** * Words that not match during session * The word that not match previous input would not match further input */ protected noMatchWords: Set = new Set() private config: SourceConfiguration private disposables: Disposable[] = [] private _disabled = false private defaults: Partial constructor(option: Partial) { // readonly properties this.name = option.name this.filepath = option.filepath || '' this.sourceType = option.sourceType || SourceType.Native this.isSnippet = !!option.isSnippet this.defaults = option this.documentSelector = option.documentSelector const key = `coc.source.${option.name}` this.config = defaultValue(workspace.initialConfiguration.get(key) as SourceConfiguration, {}) workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(key)) { this.config = defaultValue(workspace.initialConfiguration.get(key) as SourceConfiguration, {}) } }, null, this.disposables) events.on('CompleteDone', () => { this.noMatchWords.clear() }, null, this.disposables) } protected get nvim(): Neovim { return workspace.nvim } /** * Priority of source, higher priority makes items lower index. */ public get priority(): number { return this.getConfig('priority', 1) } public get triggerPatterns(): RegExp[] | null { let patterns = this.getConfig('triggerPatterns', null) if (isFalsyOrEmpty(patterns)) return null return patterns.map(s => Is.string(s) ? new RegExp(s + '$') : s) } /** * When triggerOnly is true, not trigger completion on keyword character insert. */ public get triggerOnly(): boolean { let triggerOnly = this.defaults['triggerOnly'] if (Is.boolean(triggerOnly)) return triggerOnly return Array.isArray(this.triggerPatterns) && this.triggerPatterns.length > 0 } public get triggerCharacters(): string[] { return toArray(this.getConfig('triggerCharacters', [])) } public get firstMatch(): boolean { return this.getConfig('firstMatch', true) } public get remoteFns(): string[] { return toArray(this.defaults.remoteFns) } public get shortcut(): string { let shortcut = this.getConfig('shortcut', '') return shortcut ? shortcut : this.name.slice(0, 3) } public get enable(): boolean { if (this._disabled) return false return this.getConfig('enable', true) } public get filetypes(): string[] | null { return this.getConfig('filetypes', null) } public get disableSyntaxes(): string[] { return this.getConfig('disableSyntaxes', []) } public getConfig(key: string, defaultValue?: T): T | null { let val = this.config[key] if (Is.func(val) || val == null) return defaultValue ?? null return val as T } public toggle(): void { this._disabled = !this._disabled } public get menu(): string { return '' } public async checkComplete(opt: CompleteOption): Promise { let { disableSyntaxes } = this if (!isFalsyOrEmpty(disableSyntaxes) && opt.synname) { let synname = opt.synname.toLowerCase() if (disableSyntaxes.findIndex(s => synname.includes(s.toLowerCase())) !== -1) { return false } } let fn = this.defaults['shouldComplete'] if (Is.func(fn)) return !!(await Promise.resolve(fn.call(this, opt))) return true } public async refresh(): Promise { let fn = this.defaults['refresh'] if (Is.func(fn)) await Promise.resolve(fn.call(this)) } public async onCompleteDone(item: ExtendedCompleteItem, opt: CompleteOption): Promise { let fn = this.defaults['onCompleteDone'] if (Is.func(fn)) await Promise.resolve(fn.call(this, item, opt)) } public async doComplete(opt: CompleteOption, token: CancellationToken): Promise | null> { let shouldRun = await this.checkComplete(opt) if (!shouldRun || token.isCancellationRequested) return null let fn = this.defaults['doComplete'] return await Promise.resolve(fn.call(this, opt, token)) } public async onCompleteResolve(item: ExtendedCompleteItem, opt: CompleteOption, token: CancellationToken): Promise { let fn = this.defaults['onCompleteResolve'] if (Is.func(fn)) await Promise.resolve(fn.call(this, item, opt, token)) } /** * Add words to items with timer. */ public async getResults(iterables: Iterable[], input: string, exclude: string, items: Set, token: CancellationToken): Promise { let { firstMatch, noMatchWords } = this let start = Date.now() let prev = start let len = input.length let firstCode = input.charCodeAt(0) let codes = getCharCodes(input) let ascii = isAlphabet(firstCode) let i = 0 for (let iterable of iterables) { for (let w of iterable) { i++ if (i % 100 === 0) { let curr = Date.now() if (curr - prev > 15) { await waitImmediate() prev = curr } if (token.isCancellationRequested || curr - start > MAX_DURATION) return true } if ((w.length <= 1 && w.charCodeAt(0) < 255) || w === exclude || items.has(w) || noMatchWords.has(w)) continue if (firstMatch && !firstMatchFuzzy(firstCode, ascii, w)) { noMatchWords.add(w) continue } if (len > 1) { let matched = fuzzyMatch(codes, ascii && w[0].charCodeAt(0) > ASCII_END ? unidecode(w) : w) if (!matched) { noMatchWords.add(w) continue } } items.add(w) if (items.size == MAX_COUNT) return true } } return false } public dispose(): void { disposeAll(this.disposables) } } export function firstMatchFuzzy(firstCode: number, ascii: boolean, word: string) { let ch = word[0] if (ascii && !WORD_PREFIXES_CODE.includes(firstCode) && WORD_PREFIXES.includes(ch)) ch = word[1] if (ascii && ch.charCodeAt(0) > ASCII_END) ch = unidecode(ch) return caseMatch(firstCode, ch.charCodeAt(0)) } ================================================ FILE: src/completion/sources.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import extensions from '../extension' import { createLogger } from '../logger' import BufferSync from '../model/bufferSync' import type { CompletionItemProvider, DocumentSelector } from '../provider' import { disposeAll } from '../util' import { intersect, isFalsyOrEmpty, toArray } from '../util/array' import { readFileLines, statAsync } from '../util/fs' import * as Is from '../util/is' import { fs, path, promisify } from '../util/node' import { Disposable } from '../util/protocol' import { toText } from '../util/string' import window from '../window' import workspace from '../workspace' import { KeywordsBuffer } from './keywords' import Source from './source' import LanguageSource from './source-language' import VimSource, { getMethodName } from './source-vim' import { CompleteItem, CompleteOption, ISource, SourceConfig, SourceStat, SourceType } from './types' import { getPriority } from './util' const logger = createLogger('sources') interface VimSourceConfig { filetypes?: string[] isSnippet?: boolean firstMatch?: boolean triggerCharacters?: string[] priority?: number shortcut?: string triggerOnly?: boolean } export class Sources { private sourceMap: Map = new Map() private disposables: Disposable[] = [] private remoteSourcePaths: string[] = [] public keywords: BufferSync public init(): void { this.keywords = workspace.registerBufferSync(doc => { const segmenterLocales = workspace.getConfiguration('suggest', doc).get('segmenterLocales') return new KeywordsBuffer(doc, segmenterLocales) }) this.createNativeSources() this.createRemoteSources() events.on('BufEnter', this.onDocumentEnter, this, this.disposables) events.on('CompleteDone', (_item, linenr, bufnr) => { let item = this.keywords.getItem(bufnr) if (item) item.onCompleteDone(linenr - 1) }, null, this.disposables) workspace.onDidRuntimePathChange(newPaths => { for (let p of newPaths) { this.createVimSources(p).catch(logError) } }, null, this.disposables) } private get nvim(): Neovim { return workspace.nvim } public getKeywordsBuffer(bufnr: number): KeywordsBuffer { return this.keywords.getItem(bufnr) } private createNativeSources(): void { void Promise.all([ import('../snippets/util').then(m => this.sourceMap.set(m.wordsSource.name, m.wordsSource)), import('./native/around').then(module => { module.register(this.sourceMap, this.keywords) }), import('./native/buffer').then(module => { module.register(this.sourceMap, this.keywords) }), import('./native/file').then(module => { module.register(this.sourceMap) }) ]) } public createLanguageSource( name: string, shortcut: string, selector: DocumentSelector | null, provider: CompletionItemProvider, triggerCharacters: string[], priority?: number, allCommitCharacters?: string[] ): Disposable { let source = new LanguageSource( name, shortcut, provider, selector, toArray(triggerCharacters), toArray(allCommitCharacters), priority) logger.trace('created service source', name) this.sourceMap.set(name, source) return { dispose: () => { this.sourceMap.delete(name) } } } public async createVimSourceExtension(filepath: string): Promise { let { nvim } = this try { let name = path.basename(filepath, '.vim') await nvim.command(`source ${filepath.split(path.sep).join(path.posix.sep)}`) let fns = await nvim.call('coc#_remote_fns', name) as string[] let lowercased = fns.map(fn => fn[0].toLowerCase() + fn.slice(1)) for (let fn of ['init', 'complete']) { if (!lowercased.includes(fn)) { throw new Error(`function "coc#source#${name}#${fn}" not found`) } } let props = await nvim.call(`coc#source#${name}#${getMethodName('init', fns)}`, []) as VimSourceConfig let packageJSON = { name: `coc-vim-source-${name}`, engines: { coc: ">= 0.0.1" }, activationEvents: props.filetypes ? props.filetypes.map(f => `onLanguage:${f}`) : ['*'], contributes: { configuration: { properties: { [`coc.source.${name}.enable`]: { type: 'boolean', default: true }, [`coc.source.${name}.firstMatch`]: { type: 'boolean', default: !!props.firstMatch }, [`coc.source.${name}.triggerCharacters`]: { type: 'number', default: props.triggerCharacters ?? [] }, [`coc.source.${name}.priority`]: { type: 'number', default: props.priority ?? 9 }, [`coc.source.${name}.shortcut`]: { type: 'string', default: props.shortcut ?? name.slice(0, 3).toUpperCase(), description: 'Shortcut text shown in complete menu.' }, [`coc.source.${name}.disableSyntaxes`]: { type: 'array', default: [], items: { type: 'string' } }, [`coc.source.${name}.filetypes`]: { type: 'array', default: props.filetypes || null, description: 'Enabled filetypes.', items: { type: 'string' } } } } } } let isActive = false let extension: any = { id: packageJSON.name, packageJSON, exports: void 0, extensionPath: filepath, activate: () => { isActive = true let source = new VimSource({ name, filepath, isSnippet: props.isSnippet, sourceType: SourceType.Remote, triggerOnly: !!props.triggerOnly, remoteFns: fns }) this.addSource(source) return Promise.resolve() } } Object.defineProperty(extension, 'isActive', { get: () => isActive }) await extensions.manager.registerInternalExtension(extension, () => { isActive = false this.removeSource(name) }) } catch (e) { if (!this.nvim.isVim) { let lines = await readFileLines(filepath, 0, 1) if (lines.length > 0 && lines[0].startsWith('vim9script')) return } void window.showErrorMessage(`Error on create vim source from ${filepath}: ${e}`) // logError(err) logger.error(`Error on create vim source from ${filepath}`, e) } } private createRemoteSources(): void { let paths = workspace.env.runtimepath.split(',') for (let path of paths) { this.createVimSources(path).catch(logError) } } public async createVimSources(pluginPath: string): Promise { if (this.remoteSourcePaths.includes(pluginPath) || !pluginPath) return this.remoteSourcePaths.push(pluginPath) let folder = path.join(pluginPath, 'autoload/coc/source') let stat = await statAsync(folder) if (stat && stat.isDirectory()) { let arr = await promisify(fs.readdir)(folder) let files = arr.filter(s => s.endsWith('.vim')).map(s => path.join(folder, s)) await Promise.allSettled(files.map(p => this.createVimSourceExtension(p))) } } public get names(): string[] { return Array.from(this.sourceMap.keys()) } public get sources(): ISource[] { return Array.from(this.sourceMap.values()) } public has(name): boolean { return this.names.findIndex(o => o == name) != -1 } public getSource(name: string): ISource | null { return this.sourceMap.get(name) ?? null } public shouldCommit(source: ISource | undefined, item: CompleteItem | undefined, commitCharacter: string): boolean { if (!item || source == null || commitCharacter.length === 0) return false if (Is.func(source.shouldCommit)) { return source.shouldCommit(item, commitCharacter) } return false } public getSources(opt: CompleteOption): ISource[] { let { source } = opt if (source) return toArray(this.getSource(source)) let uri = workspace.getUri(opt.bufnr) return this.getNormalSources(opt.filetype, uri) } /** * Get sources should be used without trigger. * @param {string} filetype * @returns {ISource[]} */ public getNormalSources(filetype: string, uri: string): ISource[] { let languageIds = filetype.split('.') let res = this.sources.filter(source => { let { filetypes, triggerOnly, documentSelector, enable } = source if (!enable || triggerOnly || (filetypes && !intersect(filetypes, languageIds))) return false if (documentSelector && languageIds.every(filetype => workspace.match(documentSelector, { uri, languageId: filetype }) == 0)) return false return true }) return res } private checkTrigger(source: ISource, pre: string, character: string): boolean { let { triggerCharacters, triggerPatterns } = source if (!isFalsyOrEmpty(triggerCharacters) && triggerCharacters.includes(character)) { return true } if (!isFalsyOrEmpty(triggerPatterns) && triggerPatterns.findIndex(p => p.test(pre)) !== -1) { return true } return false } public shouldTrigger(pre: string, filetype: string, uri: string): boolean { return this.getTriggerSources(pre, filetype, uri).length > 0 } public getTriggerSources(pre: string, filetype: string, uri: string, disabled: ReadonlyArray = []): ISource[] { if (!pre) return [] let character = pre[pre.length - 1] let languageIds = filetype.split('.') return this.sources.filter(source => { let { filetypes, enable, documentSelector, name } = source if (disabled.includes(name)) return false if (!enable || (Array.isArray(filetypes) && !intersect(filetypes, languageIds))) return false if (documentSelector && languageIds.every(languageId => workspace.match(documentSelector, { uri, languageId }) == 0)) { return false } return this.checkTrigger(source, pre, character) }) } public addSource(source: ISource): Disposable { let { name } = source if (this.names.includes(name)) { logger.warn(`Recreate source ${name}`) } this.sourceMap.set(name, source) return Disposable.create(() => { this.removeSource(source) }) } public removeSource(source: ISource | string): void { let name = typeof source == 'string' ? source : source.name let obj = typeof source === 'string' ? this.sourceMap.get(source) : source if (obj && typeof obj.dispose === 'function') obj.dispose() this.sourceMap.delete(name) } public async refresh(name?: string): Promise { for (let source of this.sources) { if (!name || source.name == name) { if (typeof source.refresh === 'function') { await Promise.resolve(source.refresh()) } } } } public toggleSource(name: string): void { let source = this.getSource(name) if (source && typeof source.toggle === 'function') { source.toggle() } } public sourceStats(): SourceStat[] { let stats: SourceStat[] = [] let languageSourcePriority = workspace.initialConfiguration.get('suggest.languageSourcePriority') for (let item of this.sourceMap.values()) { if (item.name === '$words') continue stats.push({ name: item.name, priority: getPriority(item, languageSourcePriority), triggerCharacters: toArray(item.triggerCharacters), shortcut: toText(item.shortcut), filetypes: toArray(item.filetypes ?? item.documentSelector?.map(o => Is.string(o) ? o : o.language)), filepath: toText(item.filepath), type: getSourceType(item.sourceType), disabled: !item.enable }) } return stats } private onDocumentEnter(bufnr: number): void { let { sources } = this for (let s of sources) { if (s.enable && Is.func(s.onEnter)) { s.onEnter(bufnr) } } } public createSource(config: SourceConfig): Disposable { if (typeof config.name !== 'string' || typeof config.doComplete !== 'function') { logger.error(`Bad config for createSource:`, config) throw new TypeError(`name and doComplete required for createSource`) } let source = new Source(Object.assign({ sourceType: SourceType.Service } as any, config)) return this.addSource(source) } public dispose(): void { disposeAll(this.disposables) } } export function logError(err: any): void { logger.error('Error on source create', err) } export function getSourceType(sourceType: SourceType): string { if (sourceType === SourceType.Native) return 'native' if (sourceType === SourceType.Remote) return 'remote' return 'service' } export default new Sources() ================================================ FILE: src/completion/types.ts ================================================ import type { CancellationToken, DocumentSelector } from 'vscode-languageserver-protocol' import type { CompletionItem, CompletionItemKind, CompletionItemLabelDetails, InsertTextFormat, InsertTextMode, Position, Range } from 'vscode-languageserver-types' import type { ProviderResult } from '../provider' import type { Documentation } from '../types' export type EditRange = Range | { insert: Range, replace: Range } export interface ItemDefaults { commitCharacters?: string[] editRange?: EditRange insertTextFormat?: InsertTextFormat insertTextMode?: InsertTextMode data?: any } // option on complete & should_complete // what need change? line, col, input, colnr, changedtick // word = '', triggerForInComplete = false export interface CompleteOption { readonly position: Position readonly bufnr: number readonly line: string col: number input: string filetype: string readonly filepath: string readonly word: string readonly followWord: string // cursor position colnr: number synname?: string readonly linenr: number readonly source?: string readonly changedtick: number readonly triggerCharacter?: string triggerForInComplete?: boolean } export interface CompleteDoneOption extends CompleteOption { readonly snippetsSupport: boolean readonly insertMode: InsertMode readonly itemDefaults?: ItemDefaults } // For filter, render and resolve export interface DurationCompleteItem { word: string abbr: string filterText: string readonly source: ISource readonly priority: number readonly shortcut?: string // start character for word insert character: number isSnippet?: boolean insertText?: string // copied from CompleteItem menu?: string kind?: string | CompletionItemKind dup?: boolean // start character for filter text delta: number preselect?: boolean sortText?: string deprecated?: boolean detail?: string labelDetails?: CompletionItemLabelDetails // Generated localBonus?: number score?: number positions?: ReadonlyArray /** * labelDetail rendered after label */ detailRendered?: boolean } export interface VimCompleteItem { word: string abbr?: string menu?: string info?: string kind?: string | CompletionItemKind equal?: number dup?: number preselect?: boolean user_data?: string detail?: string } export interface ExtendedCompleteItem extends VimCompleteItem { deprecated?: boolean labelDetails?: CompletionItemLabelDetails sortText?: string filterText?: string // could be snippet insertText?: string isSnippet?: boolean documentation?: Documentation[] } export enum InsertMode { Insert = 'insert', Replace = 'replace', } export interface CompleteConfig { asciiMatch: boolean insertMode: InsertMode autoTrigger: string filterGraceful: boolean snippetsSupport: boolean languageSourcePriority: number triggerCompletionWait: number minTriggerInputLength: number triggerAfterInsertEnter: boolean acceptSuggestionOnCommitCharacter: boolean maxItemCount: number timeout: number localityBonus: boolean highPrioritySourceLimit: number lowPrioritySourceLimit: number removeDuplicateItems: boolean removeCurrentWord: boolean defaultSortMethod: SortMethod asciiCharactersOnly: boolean enableFloat: boolean ignoreRegexps: ReadonlyArray } export enum SortMethod { None = 'none', Alphabetical = 'alphabetical', Length = 'length' } /** * Item returned from source */ export type CompleteItem = ExtendedCompleteItem | CompletionItem export interface CompleteResult { items: T[] isIncomplete?: boolean itemDefaults?: Readonly startcol?: number } export enum CompleteFinishKind { Normal = '', Confirm = 'confirm', Cancel = 'cancel', } export type CompleteDoneItem = CompleteItem & { readonly word: string readonly source: string readonly user_data: string } export enum SourceType { Native, Remote, Service, } export interface SourceStat { name: string priority: number triggerCharacters: string[] type: string shortcut: string filepath: string disabled: boolean filetypes: string[] } export interface SourceConfig { name: string triggerOnly?: boolean isSnippet?: boolean sourceType?: SourceType filepath?: string documentSelector?: DocumentSelector firstMatch?: boolean remoteFns?: string[] refresh?(): Promise toggle?(): void onEnter?(bufnr: number): void shouldComplete?(opt: CompleteOption): ProviderResult doComplete(opt: CompleteOption, token: CancellationToken): ProviderResult> onCompleteResolve?(item: T, opt: CompleteOption, token: CancellationToken): ProviderResult onCompleteDone?(item: T, opt: CompleteOption): ProviderResult shouldCommit?(item: T, character: string): boolean } export interface ISource { name: string enable?: boolean shortcut?: string priority?: number sourceType?: SourceType remoteFns?: string[] triggerCharacters?: string[] triggerOnly?: boolean triggerPatterns?: RegExp[] disableSyntaxes?: string[] isSnippet?: boolean filetypes?: string[] documentSelector?: DocumentSelector filepath?: string firstMatch?: boolean refresh?(): Promise toggle?(): void onEnter?(bufnr: number): void shouldComplete?(opt: CompleteOption): ProviderResult doComplete(opt: CompleteOption, token: CancellationToken): ProviderResult> onCompleteResolve?(item: T, opt: CompleteOption, token: CancellationToken): ProviderResult onCompleteDone?(item: T, opt: CompleteDoneOption): ProviderResult shouldCommit?(item: T, character: string): boolean dispose?(): void } ================================================ FILE: src/completion/util.ts ================================================ 'use strict' import { CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionItemTag, InsertReplaceEdit, InsertTextFormat, Range } from 'vscode-languageserver-types' import { InsertChange } from '../events' import { Chars, sameScope } from '../model/chars' import { SnippetParser } from '../snippets/parser' import { Documentation } from '../types' import { pariedCharacters } from '../util' import { isFalsyOrEmpty } from '../util/array' import { CharCode } from '../util/charCode' import { ASCII_END } from '../util/constants' import * as Is from '../util/is' import { LRUCache } from '../util/map' import { unidecode } from '../util/node' import { isEmpty, toObject } from '../util/object' import { byteIndex, byteSlice, characterIndex, getUnicodeClass, isLowSurrogate, toText } from '../util/string' import { CompleteDoneItem, CompleteItem, CompleteOption, DurationCompleteItem, EditRange, ExtendedCompleteItem, InsertMode, ISource, ItemDefaults } from './types' type MruItem = Pick, 'kind' | 'filterText' | 'source'> type PartialOption = Pick export type OptionForWord = Pick, 'line' | 'position'> export enum Selection { First = 'first', RecentlyUsed = 'recentlyUsed', RecentlyUsedByPrefix = 'recentlyUsedByPrefix' } const INVALID_WORD_CHARS = [CharCode.LineFeed, CharCode.CarriageReturn] const DollarSign = '$' const QuestionMark = '?' const MAX_CODE_POINT = 1114111 const MAX_MRU_ITEMS = 100 const DEFAULT_HL_GROUP = 'CocSymbolDefault' export interface ConvertOption { readonly insertMode: InsertMode readonly source: ISource readonly priority: number readonly range: Range readonly itemDefaults?: ItemDefaults readonly asciiMatch?: boolean } const highlightsMap = { [CompletionItemKind.Text]: 'CocSymbolText', [CompletionItemKind.Method]: 'CocSymbolMethod', [CompletionItemKind.Function]: 'CocSymbolFunction', [CompletionItemKind.Constructor]: 'CocSymbolConstructor', [CompletionItemKind.Field]: 'CocSymbolField', [CompletionItemKind.Variable]: 'CocSymbolVariable', [CompletionItemKind.Class]: 'CocSymbolClass', [CompletionItemKind.Interface]: 'CocSymbolInterface', [CompletionItemKind.Module]: 'CocSymbolModule', [CompletionItemKind.Property]: 'CocSymbolProperty', [CompletionItemKind.Unit]: 'CocSymbolUnit', [CompletionItemKind.Value]: 'CocSymbolValue', [CompletionItemKind.Enum]: 'CocSymbolEnum', [CompletionItemKind.Keyword]: 'CocSymbolKeyword', [CompletionItemKind.Snippet]: 'CocSymbolSnippet', [CompletionItemKind.Color]: 'CocSymbolColor', [CompletionItemKind.File]: 'CocSymbolFile', [CompletionItemKind.Reference]: 'CocSymbolReference', [CompletionItemKind.Folder]: 'CocSymbolFolder', [CompletionItemKind.EnumMember]: 'CocSymbolEnumMember', [CompletionItemKind.Constant]: 'CocSymbolConstant', [CompletionItemKind.Struct]: 'CocSymbolStruct', [CompletionItemKind.Event]: 'CocSymbolEvent', [CompletionItemKind.Operator]: 'CocSymbolOperator', [CompletionItemKind.TypeParameter]: 'CocSymbolTypeParameter', } export function useAscii(input: string): boolean { return input.length > 0 && input.charCodeAt(0) < ASCII_END } export function getKindText(kind: string | CompletionItemKind, kindMap: Map, defaultKindText: string): string { return Is.number(kind) ? kindMap.get(kind) ?? defaultKindText : kind } export function getKindHighlight(kind: string | number): string { return Is.number(kind) ? highlightsMap[kind] ?? DEFAULT_HL_GROUP : DEFAULT_HL_GROUP } export function getPriority(source: ISource, defaultValue: number): number { if (Is.number(source.priority)) { return source.priority } return defaultValue } export function getDetail(item: CompletionItem, filetype: string): { filetype: string, content: string } | undefined { const { detail, labelDetails, label } = item if (!isEmpty(labelDetails)) { let content = (labelDetails.detail ?? '') + (labelDetails.description ? ` ${labelDetails.description}` : '') return { filetype: 'txt', content } } if (detail && detail !== label) { let isText = /^[\w-\s.,\t\n]+$/.test(detail) return { filetype: isText ? 'txt' : filetype, content: detail } } return undefined } /** * Return 1 when next is inserted as paried character */ export function deltaCount(info: InsertChange): number { if (!info.insertChar || !info.insertChars) return 0 if (info.insertChars.length != 2) return 0 let pre = info.pre let last = pre[pre.length - 1] if (last !== info.insertChars[0] || !pariedCharacters.has(last)) return 0 let next = info.line[pre.length] if (!next || pariedCharacters.get(last) != next) return 0 return 1 } export function toCompleteDoneItem(selected: DurationCompleteItem | undefined, item: CompleteItem | undefined): CompleteDoneItem | object { if (!item || !selected) return {} return Object.assign({ word: selected.word, abbr: selected.abbr, kind: selected.kind, menu: selected.menu, source: selected.source.name, isSnippet: selected.isSnippet, user_data: `${selected.source.name}:0` }, item) } export function getDocumentations(completeItem: CompleteItem, filetype: string, detailRendered = false): Documentation[] { let docs: Documentation[] = [] if (Is.isCompletionItem(completeItem)) { let { documentation } = completeItem if (!detailRendered) { let doc = getDetail(completeItem, filetype) if (doc) docs.push(doc) } if (documentation) { if (typeof documentation == 'string') { docs.push({ filetype: 'txt', content: documentation }) } else if (documentation.value) { docs.push({ filetype: documentation.kind == 'markdown' ? 'markdown' : 'txt', content: documentation.value }) } } } else { if (completeItem.documentation) { docs = completeItem.documentation } else if (completeItem.info) { docs.push({ content: completeItem.info, filetype: 'txt' }) } } return docs } export function getResumeInput(option: PartialOption | undefined, pretext: string): string { if (!option) return null const { line, col } = option const start = characterIndex(line, col) const pl = pretext.length if (pl < start) return null for (let i = 0; i < start; i++) { // should not change content before start col. if (pretext.charCodeAt(i) !== line.charCodeAt(i)) { return null } } return byteSlice(pretext, option.col) } export function checkIgnoreRegexps(ignoreRegexps: ReadonlyArray, input: string): boolean { if (!ignoreRegexps || ignoreRegexps.length == 0 || input.length == 0) return false return ignoreRegexps.some(regexp => { try { return new RegExp(regexp).test(input) } catch (e) { return false } }) } export function createKindMap(labels: { [key: string]: string }): Map { return new Map([ [CompletionItemKind.Text, labels['text'] ?? 'v'], [CompletionItemKind.Method, labels['method'] ?? 'f'], [CompletionItemKind.Function, labels['function'] ?? 'f'], [CompletionItemKind.Constructor, typeof labels['constructor'] == 'function' ? 'f' : labels['con' + 'structor'] ?? ''], [CompletionItemKind.Field, labels['field'] ?? 'm'], [CompletionItemKind.Variable, labels['variable'] ?? 'v'], [CompletionItemKind.Class, labels['class'] ?? 'C'], [CompletionItemKind.Interface, labels['interface'] ?? 'I'], [CompletionItemKind.Module, labels['module'] ?? 'M'], [CompletionItemKind.Property, labels['property'] ?? 'm'], [CompletionItemKind.Unit, labels['unit'] ?? 'U'], [CompletionItemKind.Value, labels['value'] ?? 'v'], [CompletionItemKind.Enum, labels['enum'] ?? 'E'], [CompletionItemKind.Keyword, labels['keyword'] ?? 'k'], [CompletionItemKind.Snippet, labels['snippet'] ?? 'S'], [CompletionItemKind.Color, labels['color'] ?? 'v'], [CompletionItemKind.File, labels['file'] ?? 'F'], [CompletionItemKind.Reference, labels['reference'] ?? 'r'], [CompletionItemKind.Folder, labels['folder'] ?? 'F'], [CompletionItemKind.EnumMember, labels['enumMember'] ?? 'm'], [CompletionItemKind.Constant, labels['constant'] ?? 'v'], [CompletionItemKind.Struct, labels['struct'] ?? 'S'], [CompletionItemKind.Event, labels['event'] ?? 'E'], [CompletionItemKind.Operator, labels['operator'] ?? 'O'], [CompletionItemKind.TypeParameter, labels['typeParameter'] ?? 'T'], ]) } export function indentChanged(event: { word: string } | undefined, cursor: [number, number, string], line: string): boolean { if (!event) return false let pre = byteSlice(cursor[2], 0, cursor[1] - 1) if (pre.endsWith(event.word) && pre.match(/^\s*/)[0] != line.match(/^\s*/)[0]) { return true } return false } export function shouldStop(bufnr: number, info: InsertChange, option: Pick): boolean { let { pre } = info if (pre.length === 0 || getUnicodeClass(pre[pre.length - 1]) === 'space') return true if (option.bufnr != bufnr || option.linenr != info.lnum) return true let text = byteSlice(option.line, 0, option.col) if (!pre.startsWith(text)) return true return false } export function getInput(chars: Chars, pre: string, asciiCharactersOnly: boolean): string { let len = 0 let prev: number | undefined for (let i = pre.length - 1; i >= 0; i--) { let code = pre.charCodeAt(i) let word = isWordCode(chars, code, asciiCharactersOnly) if (!word || (prev !== undefined && !sameScope(prev, code))) { break } len += 1 prev = code } return len == 0 ? '' : pre.slice(-len) } export function isWordCode(chars: Chars, code: number, asciiCharactersOnly: boolean): boolean { if (!chars.isKeywordCode(code)) return false if (isLowSurrogate(code)) return false if (asciiCharactersOnly && code >= 255) return false return true } export function shouldIndent(indentkeys: string, pretext: string): boolean { if (!indentkeys || pretext.trim().includes(' ')) return false for (let part of indentkeys.split(',')) { if (part.indexOf('=') > -1) { let [pre, post] = part.split('=') let word = post.startsWith('~') ? post.slice(1) : post if (pretext.length < word.length || (pretext.length > word.length && !/^\s/.test(pretext.slice(-word.length - 1)))) { continue } let matched = post.startsWith('~') ? pretext.toLowerCase().endsWith(word) : pretext.endsWith(word) if (!matched) { continue } if (pre == '') return true if (pre == '0' && /^\s*$/.test(pretext.slice(0, pretext.length - word.length))) { return true } } } return false } export function highlightOffset(pre: number, item: T): number { let { filterText, abbr } = item let idx = abbr.indexOf(filterText) if (idx == -1) return -1 let n = idx == 0 ? 0 : byteIndex(abbr, idx) return pre + n } export function emptLabelDetails(labelDetails: CompletionItemLabelDetails): boolean { if (!labelDetails) return true return !labelDetails.detail && !labelDetails.description } export function isSnippetItem(item: CompletionItem, itemDefaults: ItemDefaults): boolean { let insertTextFormat = item.insertTextFormat ?? itemDefaults.insertTextFormat return insertTextFormat === InsertTextFormat.Snippet } /** * Snippet or have additionalTextEdits */ export function hasAction(item: CompletionItem, itemDefaults: ItemDefaults) { return isSnippetItem(item, itemDefaults) || !isFalsyOrEmpty(item.additionalTextEdits) } function toValidWord(snippet: string, excludes: number[]): string { for (let i = 0; i < snippet.length; i++) { let code = snippet.charCodeAt(i) if (excludes.includes(code)) { return snippet.slice(0, i) } } return snippet } function snippetToWord(text: string, kind: CompletionItemKind | undefined): string { if (kind === CompletionItemKind.Function || kind === CompletionItemKind.Method || kind === CompletionItemKind.Class) { text = text.replace(/\(.+/, '') } if (!text.includes(DollarSign)) return text return toValidWord((new SnippetParser()).text(text), INVALID_WORD_CHARS) } /** * Get the word to insert, it's the word to insert from range or input start position, * may not the actual word to insert */ export function getWord(item: CompletionItem, itemDefaults: ItemDefaults): string { let { label, data, kind } = item if (data && Is.string(data.word)) return data.word let textToInsert = item.textEdit ? item.textEdit.newText : item.insertText if (!Is.string(textToInsert)) return label return isSnippetItem(item, itemDefaults) ? snippetToWord(textToInsert, kind) : toValidWord(textToInsert, INVALID_WORD_CHARS) } export function getReplaceRange(item: CompletionItem, defaultRange: EditRange | undefined, character?: number, insertMode?: InsertMode): Range | undefined { let editRange: EditRange | undefined if (item.textEdit) { editRange = InsertReplaceEdit.is(item.textEdit) ? item.textEdit : item.textEdit.range } else if (defaultRange) { editRange = defaultRange } let range: Range | undefined if (editRange) { if (Range.is(editRange)) { range = editRange } else { range = insertMode == InsertMode.Insert ? editRange.insert : editRange.replace } } // start character must contains character for completion if (range && Is.number(character) && range.start.character > character) range.start.character = character return range } export class Converter { // cache the sliced text private previousCache: Map = new Map() private postCache: Map = new Map() // cursor position private character: number public minCharacter = Number.MAX_SAFE_INTEGER private inputLen: number constructor( // input start character index private readonly inputStart: number, private readonly option: ConvertOption, private readonly opt: OptionForWord ) { this.character = opt.position.character this.inputLen = opt.position.character - inputStart } /** * Text before input to replace */ public getPrevious(character: number): string { if (this.previousCache.has(character)) return this.previousCache.get(character) let prev = this.opt.line.slice(character, this.inputStart) this.previousCache.set(character, prev) return prev } /** * Text after cursor to replace */ public getAfter(character: number): string { if (this.postCache.has(character)) return this.postCache.get(character) let text = this.opt.line.slice(this.character, character) this.postCache.set(character, text) return text } /** * Exclude follow characters to replace from end of word */ public fixFollow(word: string, isSnippet: boolean, endCharacter: number): string { if (isSnippet || endCharacter <= this.character) return word let toReplace = this.getAfter(endCharacter) if (word.length - this.inputLen > toReplace.length && word.endsWith(toReplace)) { return word.slice(0, - toReplace.length) } return word } /** * Better filter text with prefix before input removed if exists. */ private getDelta(filterText: string, character: number): number { if (character < this.inputStart) { let prev = this.getPrevious(character) if (filterText.startsWith(prev)) return prev.length } return 0 } public convertToDurationItem(item: CompleteItem): DurationCompleteItem | undefined { if (Is.isCompletionItem(item)) { return this.convertLspCompleteItem(item) } else if (Is.string(item.word)) { return this.convertVimCompleteItem(item) } return undefined } private convertVimCompleteItem(item: ExtendedCompleteItem): DurationCompleteItem { const { option } = this const { range, asciiMatch } = option const word = toText(item.word) const character = range.start.character this.minCharacter = Math.min(this.minCharacter, character) let filterText = item.filterText ?? word filterText = asciiMatch ? unidecode(filterText) : filterText const delta = this.getDelta(filterText, character) return { word: this.fixFollow(word, item.isSnippet, range.end.character), abbr: item.abbr ?? word, filterText, delta, character, dup: item.dup === 1, menu: item.menu, kind: item.kind, isSnippet: !!item.isSnippet, insertText: item.insertText, preselect: item.preselect, sortText: item.sortText, deprecated: item.deprecated, detail: item.detail, labelDetails: item.labelDetails, get source() { return option.source }, get priority() { return option.source.priority ?? 99 }, get shortcut() { return toText(option.source.shortcut) } } } private convertLspCompleteItem(item: CompletionItem): DurationCompleteItem { const { option, inputStart } = this const label = item.label.trim() const itemDefaults = toObject(option.itemDefaults) as ItemDefaults const word = getWord(item, itemDefaults) const range = getReplaceRange(item, itemDefaults?.editRange, inputStart, this.option.insertMode) ?? option.range const character = range.start.character const data = toObject(item.data) const filterText = item.filterText ?? item.label const delta = this.getDelta(filterText, character) let obj: DurationCompleteItem = { // the word to be insert from it's own character. word: this.fixFollow(word, isSnippetItem(item, itemDefaults), range.end.character), abbr: label, character, delta, kind: item.kind, detail: item.detail, sortText: item.sortText, filterText, preselect: item.preselect === true, deprecated: item.deprecated === true || item.tags?.includes(CompletionItemTag.Deprecated), isSnippet: hasAction(item, itemDefaults), get source() { return option.source }, get priority() { return option.priority }, get shortcut() { return toText(option.source.shortcut) }, dup: data.dup !== 0 } this.minCharacter = Math.min(this.minCharacter, character) if (data.optional && !obj.abbr.endsWith(QuestionMark)) obj.abbr += QuestionMark if (!emptLabelDetails(item.labelDetails)) obj.labelDetails = item.labelDetails if (Is.number(item['score']) && !obj.sortText) obj.sortText = String.fromCodePoint(MAX_CODE_POINT - Math.round(item['score'])) return obj } } function toItemKey(item: MruItem): string { let label = item.filterText let source = item.source.name let kind = item.kind ?? '' return `${label}|${source}|${kind}` } export class MruLoader { private max = 0 private items: LRUCache = new LRUCache(MAX_MRU_ITEMS) private itemsNoPrefix: LRUCache = new LRUCache(MAX_MRU_ITEMS) public getScore(input: string, item: MruItem, selection: Selection): number { let key = toItemKey(item) if (input.length == 0) return this.itemsNoPrefix.get(key) ?? -1 if (selection === Selection.RecentlyUsedByPrefix) key = `${input}|${key}` let map = selection === Selection.RecentlyUsed ? this.itemsNoPrefix : this.items return map.get(key) ?? -1 } public add(prefix: string, item: MruItem): void { if (!Is.number(item.kind)) return let key = toItemKey(item) if (!item.filterText.startsWith(prefix)) { prefix = '' } let line = `${prefix}|${key}` this.items.set(line, this.max) this.itemsNoPrefix.set(key, this.max) this.max += 1 } public clear(): void { this.max = 0 this.items.clear() this.itemsNoPrefix.clear() } } ================================================ FILE: src/completion/wordDistance.ts ================================================ import { CompletionItemKind, Position, Range, SelectionRange } from 'vscode-languageserver-types' import events from '../events' import languages from '../languages' import { CompleteOption, DurationCompleteItem } from './types' import { binarySearch, isFalsyOrEmpty, toArray } from '../util/array' import { equals, toObject } from '../util/object' import * as Is from '../util/is' import { compareRangesUsingStarts, rangeInRange } from '../util/position' import { CancellationToken } from '../util/protocol' import workspace from '../workspace' import { waitWithToken } from '../util' export abstract class WordDistance { public static readonly None = new class extends WordDistance { public distance() { return 0 } }() public static async create(localityBonus: boolean, opt: Pick, token: CancellationToken): Promise { let { position } = opt let cursor: [number, number] = [opt.linenr, opt.colnr] if (!localityBonus) return WordDistance.None let doc = workspace.getDocument(opt.bufnr) const selectionRanges = await languages.getSelectionRanges(doc.textDocument, [position], token) if (!selectionRanges || token.isCancellationRequested) return WordDistance.None let ranges: Range[] = [] const iterate = (r?: SelectionRange) => { if (r && r.range.end.line - r.range.start.line < 2000) { ranges.unshift(r.range) iterate(r.parent) } } iterate(toArray(selectionRanges)[0]) let wordRanges = ranges.length > 0 ? await Promise.race([waitWithToken(100, token), workspace.computeWordRanges(opt.bufnr, ranges[0], token)]) : undefined if (!Is.objectLiteral(wordRanges)) return WordDistance.None // remove current word delete wordRanges[opt.word] return new class extends WordDistance { // Unlike VSCode, word insert position is used here public distance(anchor: Position, item: DurationCompleteItem) { if (!equals([events.cursor.lnum, events.cursor.col], cursor)) { return 0 } if (item.kind === CompletionItemKind.Keyword || toObject(item.source)['name'] === 'snippets') { return 2 << 20 } const wordLines = wordRanges[item.word] if (isFalsyOrEmpty(wordLines)) { return 2 << 20 } const idx = binarySearch(wordLines, Range.create(anchor, anchor), compareRangesUsingStarts) const bestWordRange = idx >= 0 ? wordLines[idx] : wordLines[Math.max(0, ~idx - 1)] let blockDistance = ranges.length for (const range of ranges) { if (!rangeInRange(bestWordRange, range)) { break } blockDistance -= 1 } return blockDistance } }() } public abstract distance(anchor: Position, item: DurationCompleteItem): number } ================================================ FILE: src/configuration/configuration.ts ================================================ 'use strict' import { URI } from 'vscode-uri' import { distinct } from '../util/array' import { isParentFolder, normalizeFilePath, sameFile } from '../util/fs' import { equals } from '../util/object' import { ConfigurationModel } from './model' import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationModel, IConfigurationOverrides } from './types' import { compareConfigurationContents, IConfigurationCompareResult, overrideIdentifiersFromKey } from './util' export interface IConfigurationValue { readonly defaultValue?: T readonly userValue?: T readonly workspaceValue?: T readonly workspaceFolderValue?: T readonly memoryValue?: T readonly value?: T readonly default?: { value?: T; override?: T } readonly user?: { value?: T; override?: T } readonly workspace?: { value?: T; override?: T } readonly workspaceFolder?: { value?: T; override?: T } readonly memory?: { value?: T; override?: T } readonly overrideIdentifiers?: string[] } export class FolderConfigutions { private _folderConfigurations: Map = new Map() public get keys(): Iterable { return this._folderConfigurations.keys() } public has(folder: string): boolean { for (let key of this.keys) { if (sameFile(folder, key)) return true } return false } public set(folder: string, model: ConfigurationModel): void { let key = normalizeFilePath(folder) this._folderConfigurations.set(key, model) } public get(folder: string): ConfigurationModel | undefined { let key = normalizeFilePath(folder) return this._folderConfigurations.get(key) } public delete(folder: string): void { let key = normalizeFilePath(folder) this._folderConfigurations.delete(key) } public forEach(fn: (model: ConfigurationModel, key: string) => void): void { this._folderConfigurations.forEach(fn) } public getConfigurationByResource(uri: string): { folder: string, model: ConfigurationModel } | undefined { let u = URI.parse(uri) if (u.scheme !== 'file') return undefined let folders = Array.from(this._folderConfigurations.keys()) folders.sort((a, b) => b.length - a.length) let fullpath = u.fsPath for (let folder of folders) { if (isParentFolder(folder, fullpath, true)) { return { folder, model: this._folderConfigurations.get(folder) } } } return undefined } } export class Configuration { private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null private _resolvedFolderConfigurations: Map = new Map() private _memoryConfigurationByResource: Map = new Map() constructor( private _defaultConfiguration: ConfigurationModel, private _userConfiguration: ConfigurationModel, private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), private _folderConfigurations: FolderConfigutions = new FolderConfigutions(), private _memoryConfiguration: ConfigurationModel = new ConfigurationModel() ) { } public updateValue(key: string, value: any, overrides: IConfigurationOverrides = {}): void { let memoryConfiguration: ConfigurationModel | undefined if (overrides.resource) { memoryConfiguration = this._memoryConfigurationByResource.get(overrides.resource) if (!memoryConfiguration) { memoryConfiguration = new ConfigurationModel() this._memoryConfigurationByResource.set(overrides.resource, memoryConfiguration) } } else { memoryConfiguration = this._memoryConfiguration } if (value === undefined) { memoryConfiguration.removeValue(key) } else { memoryConfiguration.setValue(key, value) } if (!overrides.resource) { this._workspaceConsolidatedConfiguration = null } } public hasFolder(folder: string): boolean { return this._folderConfigurations.has(folder) } public addFolderConfiguration(folder: string, model: ConfigurationModel, resource?: string): void { this._folderConfigurations.set(folder, model) if (resource) { this._resolvedFolderConfigurations.set(resource, folder) } } public deleteFolderConfiguration(fsPath: string): void { this._folderConfigurations.delete(fsPath) } private getWorkspaceConsolidateConfiguration(): ConfigurationModel { if (!this._workspaceConsolidatedConfiguration) { this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this._userConfiguration, this._workspaceConfiguration, this._memoryConfiguration) this._workspaceConsolidatedConfiguration = this._workspaceConsolidatedConfiguration.freeze() } return this._workspaceConsolidatedConfiguration } /** * Get folder configuration fsPath & model * @param uri folder or file uri */ public getFolderConfigurationModelForResource(uri: string): ConfigurationModel | undefined { let folder = this._resolvedFolderConfigurations.get(uri) if (folder) return this._folderConfigurations.get(folder) let conf = this._folderConfigurations.getConfigurationByResource(uri) if (!conf) return undefined this._resolvedFolderConfigurations.set(uri, conf.folder) return conf.model } public resolveFolder(uri: string): string | undefined { let folder = this._resolvedFolderConfigurations.get(uri) if (folder) return folder let folders = Array.from(this._folderConfigurations.keys) folders.sort((a, b) => b.length - a.length) for (let folder of folders) { if (isParentFolder(folder, URI.parse(uri).fsPath, true)) { this._resolvedFolderConfigurations.set(uri, folder) return folder } } return undefined } private getConsolidatedConfigurationModel(overrides: IConfigurationOverrides): ConfigurationModel { let configuration = this.getWorkspaceConsolidateConfiguration() if (overrides.resource) { let folderConfiguration = this.getFolderConfigurationModelForResource(overrides.resource) if (folderConfiguration) { configuration = configuration.merge(folderConfiguration) } const memoryConfigurationForResource = this._memoryConfigurationByResource.get(overrides.resource) if (memoryConfigurationForResource) { configuration = configuration.merge(memoryConfigurationForResource) } } if (overrides.overrideIdentifier) { configuration = configuration.override(overrides.overrideIdentifier) } return configuration } public getValue(section: string | undefined, overrides: IConfigurationOverrides): any { let configuration = this.getConsolidatedConfigurationModel(overrides) return configuration.getValue(section) } public inspect(key: string, overrides: IConfigurationOverrides): IConfigurationValue { const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides) const folderConfigurationModel = this.getFolderConfigurationModelForResource(overrides.resource) const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration const defaultValue = overrides.overrideIdentifier ? this._defaultConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._defaultConfiguration.freeze().getValue(key) const userValue = overrides.overrideIdentifier ? this._userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._userConfiguration.freeze().getValue(key) const workspaceValue = overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._workspaceConfiguration.freeze().getValue(key) const workspaceFolderValue = folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue(key) : folderConfigurationModel.freeze().getValue(key) : undefined const memoryValue = overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue(key) : memoryConfigurationModel.getValue(key) const value = consolidateConfigurationModel.getValue(key) const overrideIdentifiers: string[] = distinct(consolidateConfigurationModel.overrides.map(override => override.identifiers).flat()).filter(overrideIdentifier => consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined) return { defaultValue, userValue, workspaceValue, workspaceFolderValue, memoryValue, value, default: defaultValue !== undefined ? { value: this._defaultConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._defaultConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, user: userValue !== undefined ? { value: this._userConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._userConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, workspace: workspaceValue !== undefined ? { value: this._workspaceConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, workspaceFolder: workspaceFolderValue !== undefined ? { value: folderConfigurationModel?.freeze().getValue(key), override: overrides.overrideIdentifier ? folderConfigurationModel?.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, memory: memoryValue !== undefined ? { value: memoryConfigurationModel.getValue(key), override: overrides.overrideIdentifier ? memoryConfigurationModel.getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, overrideIdentifiers: overrideIdentifiers.length ? overrideIdentifiers : undefined } } public get defaults(): ConfigurationModel { return this._defaultConfiguration } public get user(): ConfigurationModel { return this._userConfiguration } public get workspace(): ConfigurationModel { return this._workspaceConfiguration } public get memory(): ConfigurationModel { return this._memoryConfiguration } public getConfigurationModel(target: ConfigurationTarget, folder?: string): ConfigurationModel { switch (target) { case ConfigurationTarget.Default: return this._defaultConfiguration case ConfigurationTarget.User: return this._userConfiguration case ConfigurationTarget.Workspace: return this._workspaceConfiguration case ConfigurationTarget.WorkspaceFolder: return this._folderConfigurations.get(folder) ?? new ConfigurationModel() default: return this._memoryConfiguration } } public updateFolderConfiguration(folder: string, model: ConfigurationModel): void { this._folderConfigurations.set(folder, model) } public updateUserConfiguration(model: ConfigurationModel): void { this._userConfiguration = model this._workspaceConsolidatedConfiguration = null } public updateWorkspaceConfiguration(model: ConfigurationModel): void { this._workspaceConfiguration = model this._workspaceConsolidatedConfiguration = null } public updateDefaultConfiguration(model: ConfigurationModel): void { this._defaultConfiguration = model this._workspaceConsolidatedConfiguration = null } public updateMemoryConfiguration(model: ConfigurationModel): void { this._memoryConfiguration = model this._workspaceConsolidatedConfiguration = null } public compareAndUpdateMemoryConfiguration(memory: ConfigurationModel): IConfigurationChange { const { added, updated, removed, overrides } = compare(this._memoryConfiguration, memory) const keys = [...added, ...updated, ...removed] if (keys.length) { this.updateMemoryConfiguration(memory) } return { keys, overrides } } public compareAndUpdateUserConfiguration(user: ConfigurationModel): IConfigurationChange { const { added, updated, removed, overrides } = compare(this._userConfiguration, user) const keys = [...added, ...updated, ...removed] if (keys.length) { this.updateUserConfiguration(user) } return { keys, overrides } } public compareAndUpdateDefaultConfiguration(defaults: ConfigurationModel, keys?: string[]): IConfigurationChange { const overrides: [string, string[]][] = [] if (!keys) { const { added, updated, removed } = compare(this._defaultConfiguration, defaults) keys = [...added, ...updated, ...removed] } for (const key of keys) { for (const overrideIdentifier of overrideIdentifiersFromKey(key)) { const fromKeys = this._defaultConfiguration.getKeysForOverrideIdentifier(overrideIdentifier) const toKeys = defaults.getKeysForOverrideIdentifier(overrideIdentifier) const keys = [ ...toKeys.filter(key => fromKeys.indexOf(key) === -1), ...fromKeys.filter(key => toKeys.indexOf(key) === -1), ...fromKeys.filter(key => !equals(this._defaultConfiguration.override(overrideIdentifier).getValue(key), defaults.override(overrideIdentifier).getValue(key))) ] overrides.push([overrideIdentifier, keys]) } } this.updateDefaultConfiguration(defaults) return { keys, overrides } } public compareAndUpdateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel): IConfigurationChange { const { added, updated, removed, overrides } = compare(this._workspaceConfiguration, workspaceConfiguration) const keys = [...added, ...updated, ...removed] if (keys.length) { this.updateWorkspaceConfiguration(workspaceConfiguration) } return { keys, overrides } } public compareAndUpdateFolderConfiguration(folder: string, folderConfiguration: ConfigurationModel): IConfigurationChange { const currentFolderConfiguration = this._folderConfigurations.get(folder) const { added, updated, removed, overrides } = compare(currentFolderConfiguration, folderConfiguration) const keys = [...added, ...updated, ...removed] if (keys.length || !currentFolderConfiguration) { this.updateFolderConfiguration(folder, folderConfiguration) } return { keys, overrides } } public compareAndDeleteFolderConfiguration(folder: string): IConfigurationChange { const folderConfig = this._folderConfigurations.get(folder) if (!folderConfig) return this.deleteFolderConfiguration(folder) const { added, updated, removed, overrides } = compare(folderConfig, undefined) return { keys: [...added, ...updated, ...removed], overrides } } public allKeys(): string[] { const keys: Set = new Set() this._defaultConfiguration.freeze().keys.forEach(key => keys.add(key)) this._userConfiguration.freeze().keys.forEach(key => keys.add(key)) this._workspaceConfiguration.freeze().keys.forEach(key => keys.add(key)) this._folderConfigurations.forEach(folderConfiguration => folderConfiguration.freeze().keys.forEach(key => keys.add(key))) return [...keys.values()] } public toData(): IConfigurationData { let { _defaultConfiguration, _memoryConfiguration, _userConfiguration, _workspaceConfiguration, _folderConfigurations } = this let folders: [string, IConfigurationModel][] = [] _folderConfigurations.forEach((model, fsPath) => { folders.push([fsPath, model.toJSON()]) }) return { defaults: _defaultConfiguration.toJSON(), user: _userConfiguration.toJSON(), workspace: _workspaceConfiguration.toJSON(), folders, memory: _memoryConfiguration.toJSON() } } public static parse(data: IConfigurationData): Configuration { const defaultConfiguration = this.parseConfigurationModel(data.defaults) const userConfiguration = this.parseConfigurationModel(data.user) const workspaceConfiguration = this.parseConfigurationModel(data.workspace) const folderConfigurations: FolderConfigutions = new FolderConfigutions() const memoryConfiguration = this.parseConfigurationModel(data.memory) data.folders.forEach(value => { folderConfigurations.set(value[0], this.parseConfigurationModel(value[1])) }) return new Configuration(defaultConfiguration, userConfiguration, workspaceConfiguration, folderConfigurations, memoryConfiguration) } private static parseConfigurationModel(model: IConfigurationModel): ConfigurationModel { return new ConfigurationModel(model.contents, model.keys, model.overrides).freeze() } } function compare(from: ConfigurationModel | undefined, to: ConfigurationModel | undefined): IConfigurationCompareResult { const { added, removed, updated } = compareConfigurationContents(to, from) const overrides: [string, string[]][] = [] const fromOverrideIdentifiers = from?.getAllOverrideIdentifiers() ?? [] const toOverrideIdentifiers = to?.getAllOverrideIdentifiers() ?? [] if (to) { const addedOverrideIdentifiers = toOverrideIdentifiers.filter(key => !fromOverrideIdentifiers.includes(key)) for (const identifier of addedOverrideIdentifiers) { overrides.push([identifier, to.getKeysForOverrideIdentifier(identifier)]) } } if (from) { const removedOverrideIdentifiers = fromOverrideIdentifiers.filter(key => !toOverrideIdentifiers.includes(key)) for (const identifier of removedOverrideIdentifiers) { overrides.push([identifier, from.getKeysForOverrideIdentifier(identifier)]) } } if (to && from) { for (const identifier of fromOverrideIdentifiers) { if (toOverrideIdentifiers.includes(identifier)) { const result = compareConfigurationContents({ contents: from.getOverrideValue(undefined, identifier) || {}, keys: from.getKeysForOverrideIdentifier(identifier) }, { contents: to.getOverrideValue(undefined, identifier) || {}, keys: to.getKeysForOverrideIdentifier(identifier) }) overrides.push([identifier, [...result.added, ...result.removed, ...result.updated]]) } } } return { added, removed, updated, overrides } } ================================================ FILE: src/configuration/event.ts ================================================ 'use strict' import { equals } from '../util/object' import { Configuration } from './configuration' import { ConfigurationModel } from './model' import type { ConfigurationResourceScope, ConfigurationTarget, IConfigurationChange, IConfigurationChangeEvent, IConfigurationData } from './types' import { scopeToOverrides, toValuesTree } from './util' export class ConfigurationChangeEvent implements IConfigurationChangeEvent { private readonly affectedKeysTree: any public readonly affectedKeys: string[] public source: ConfigurationTarget // public sourceConfig: any constructor(public readonly change: IConfigurationChange, private readonly previous: IConfigurationData | undefined, private readonly currentConfiguration: Configuration) { const keysSet = new Set() change.keys.forEach(key => keysSet.add(key)) change.overrides.forEach(([, keys]) => keys.forEach(key => keysSet.add(key))) this.affectedKeys = [...keysSet.values()] const configurationModel = new ConfigurationModel() this.affectedKeys.forEach(key => configurationModel.setValue(key, {})) this.affectedKeysTree = configurationModel.contents } private _previousConfiguration: Configuration | undefined = undefined private get previousConfiguration(): Configuration | undefined { if (!this._previousConfiguration && this.previous) { this._previousConfiguration = Configuration.parse(this.previous) } return this._previousConfiguration } public affectsConfiguration(section: string, scope?: ConfigurationResourceScope): boolean { let overrides = scope ? scopeToOverrides(scope) : undefined if (this.doesAffectedKeysTreeContains(this.affectedKeysTree, section)) { if (overrides) { const value1 = this.previousConfiguration ? this.previousConfiguration.getValue(section, overrides) : undefined const value2 = this.currentConfiguration.getValue(section, overrides) return !equals(value1, value2) } return true } return false } private doesAffectedKeysTreeContains(affectedKeysTree: any, section: string): boolean { let requestedTree = toValuesTree({ [section]: true }, () => {}) let key while (typeof requestedTree === 'object' && (key = Object.keys(requestedTree)[0])) { // Only one key should present, since we added only one property affectedKeysTree = affectedKeysTree[key] if (!affectedKeysTree) { return false // Requested tree is not found } requestedTree = requestedTree[key] } return true } } export class AllKeysConfigurationChangeEvent extends ConfigurationChangeEvent { constructor(configuration: Configuration, source: ConfigurationTarget) { super({ keys: configuration.allKeys(), overrides: [] }, undefined, configuration) this.source = source } } ================================================ FILE: src/configuration/index.ts ================================================ 'use strict' import { Diagnostic } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import defaultSchema from '../../data/schema.json' import { createLogger } from '../logger' import { disposeAll } from '../util' import { isFalsyOrEmpty } from '../util/array' import { CONFIG_FILE_NAME } from '../util/constants' import { getExtensionDefinitions } from '../util/extensionRegistry' import { findUp, normalizeFilePath, sameFile, watchFile } from '../util/fs' import { objectLiteral } from '../util/is' import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../util/jsonRegistry' import { IJSONSchema } from '../util/jsonSchema' import { fs, os, path } from '../util/node' import { deepFreeze, hasOwnProperty, mixin } from '../util/object' import { Disposable, Emitter, Event } from '../util/protocol' import { convertProperties, Registry } from '../util/registry' import { Configuration } from './configuration' import { ConfigurationChangeEvent } from './event' import { ConfigurationModel } from './model' import { ConfigurationModelParser } from './parser' import { allSettings, Extensions, IConfigurationNode, IConfigurationRegistry, resourceSettings } from './registry' import { IConfigurationShape } from './shape' import { ConfigurationInspect, ConfigurationResourceScope, ConfigurationTarget, ConfigurationUpdateTarget, IConfigurationChange, IConfigurationChangeEvent, IConfigurationOverrides, WorkspaceConfiguration } from './types' import { addToValueTree, convertTarget, lookUp, scopeToOverrides } from './util' const logger = createLogger('configurations') export const userSettingsSchemaId = 'vscode://schemas/settings/user' export const folderSettingsSchemaId = 'vscode://schemas/settings/folder' const jsonRegistry = Registry.as(JSONExtensions.JSONContribution) const configuration = Registry.as(Extensions.Configuration) interface ConfigurationErrorEvent { uri: string, diagnostics: Diagnostic[] } interface MarkdownPreference { excludeImages: boolean breaks: boolean } export default class Configurations { private _watchedFiles: Set = new Set() private _configuration: Configuration private _errors: Map = new Map() private _onError = new Emitter() private _onChange = new Emitter() private disposables: Disposable[] = [] private _initialized = false private cached: IConfigurationNode[] = [] private _initialConfiguration: WorkspaceConfiguration public readonly onError: Event = this._onError.event public readonly onDidChange: Event = this._onChange.event constructor( private userConfigFile?: string | undefined, private readonly _proxy?: IConfigurationShape, private noWatch = global.__TEST__, cwd = process.cwd() ) { let defaultConfiguration = this.loadDefaultConfigurations() let userConfiguration = this.parseConfigurationModel(this.userConfigFile) this._configuration = new Configuration(defaultConfiguration, userConfiguration) this.watchFile(this.userConfigFile, ConfigurationTarget.User) let filepath = this.folderToConfigfile(cwd) if (filepath) this.addFolderFile(filepath, true) this._initialConfiguration = this.getConfiguration(undefined, null) } /** * Contains default, memory and user configuration only */ public get initialConfiguration(): WorkspaceConfiguration { return this._initialConfiguration } public get markdownPreference(): MarkdownPreference { let preferences = this._initialConfiguration.get('coc.preferences') as any return { excludeImages: preferences.excludeImageLinksInMarkdownDocument, breaks: preferences.enableGFMBreaksInMarkdownDocument } } public get errors(): Map { return this._errors } public get configuration(): Configuration { return this._configuration } public flushConfigurations(): void { this._initialized = true configuration.registerConfigurations(this.cached) this.cached = [] } public updateConfigurations(add: IConfigurationNode[], remove?: IConfigurationNode[]): void { if (this._initialized) { if (!isFalsyOrEmpty(remove)) { configuration.updateConfigurations({ add, remove }) } else { configuration.registerConfigurations(add) } } else { this.cached.push(...add) } } private loadDefaultConfigurations(): ConfigurationModel { // register properties and listen events let node: IConfigurationNode = { properties: convertProperties(defaultSchema.properties) } configuration.registerConfiguration(node) configuration.onDidUpdateConfiguration(e => { if (e.properties.length === 0) return // update default configuration with new value const dict = configuration.getConfigurationProperties() const toRemove: string[] = [] const root = Object.create(null) const keys: string[] = [] for (let key of e.properties) { let def = dict[key] if (def) { keys.push(key) let val = def.default addToValueTree(root, key, val, msg => { logger.error(`Conflict configuration: ${msg}`) }) } else { toRemove.push(key) } } const model = this._configuration.defaults.merge(new ConfigurationModel(root, keys)) toRemove.forEach(key => { model.removeValue(key) }) if (!this._initialized) { // no change event fired this._configuration.updateDefaultConfiguration(model) this._initialConfiguration = this.getConfiguration(undefined, null) } else { this.changeConfiguration(ConfigurationTarget.Default, model, undefined, e.properties) } }, null, this.disposables) let properties = configuration.getConfigurationProperties() let config = {} let keys: string[] = [] Object.keys(properties).forEach(key => { let value = properties[key].default keys.push(key) addToValueTree(config, key, value, undefined) }) let model = new ConfigurationModel(config, keys) return model } public getDescription(key: string): string | undefined { let property = allSettings.properties[key] return property ? property.description : undefined } public getJSONSchema(uri: string): IJSONSchema | undefined { if (uri === userSettingsSchemaId) { return { properties: allSettings.properties, patternProperties: allSettings.patternProperties, definitions: Object.assign(getExtensionDefinitions(), defaultSchema.definitions), additionalProperties: false, allowTrailingCommas: true, allowComments: true } } if (uri === folderSettingsSchemaId) { return { properties: resourceSettings.properties, patternProperties: resourceSettings.patternProperties, definitions: Object.assign(getExtensionDefinitions(), defaultSchema.definitions), errorMessage: 'Configuration property may not work as folder configuration', additionalProperties: false, allowTrailingCommas: true, allowComments: true } } let schemas = jsonRegistry.getSchemaContributions().schemas if (hasOwnProperty(schemas, uri)) return schemas[uri] return undefined } public parseConfigurationModel(filepath: string | undefined, filecontents?: string): ConfigurationModel { if (!filepath || !fs.existsSync(filepath)) return new ConfigurationModel() let parser = new ConfigurationModelParser(filepath) let content = filecontents || fs.readFileSync(filepath, 'utf8') let uri = URI.file(filepath).toString() parser.parse(content) if (!isFalsyOrEmpty(parser.errors)) { this._errors.set(uri, parser.errors) this._onError.fire({ uri, diagnostics: parser.errors }) } else { this._errors.delete(uri) this._onError.fire({ uri, diagnostics: [] }) } return parser.configurationModel } public folderToConfigfile(folder: string): string | undefined { if (sameFile(folder, os.homedir())) return undefined let filepath = path.join(folder, '.vim', CONFIG_FILE_NAME) if (sameFile(filepath, this.userConfigFile)) return undefined return filepath } // change memory configuration public updateMemoryConfig(props: { [key: string]: any }): void { let keys = Object.keys(props) if (!props || keys.length == 0) return let memoryModel = this._configuration.memory.clone() let properties = configuration.getConfigurationProperties() keys.forEach(key => { let val = props[key] if (val === undefined) { memoryModel.removeValue(key) } else if (properties[key] != null) { memoryModel.setValue(key, val) } else if (objectLiteral(val)) { for (let k of Object.keys(val)) { memoryModel.setValue(`${key}.${k}`, val[k]) } } else { memoryModel.setValue(key, val) } }) this.changeConfiguration(ConfigurationTarget.Memory, memoryModel, undefined, keys) } /** * Add new folder config file. */ public addFolderFile(configFilePath: string, fromCwd = false, resource?: string): boolean { let folder = normalizeFilePath(path.resolve(configFilePath, '../..')) if (this._configuration.hasFolder(folder) || !fs.existsSync(configFilePath)) return false let configFile: string try { configFile = fs.readFileSync(configFilePath, 'utf8') } catch (_err) { return false } this.watchFile(configFilePath, ConfigurationTarget.WorkspaceFolder) let model = this.parseConfigurationModel(configFilePath, configFile) this._configuration.addFolderConfiguration(folder, model, resource) logger.info(`Add folder configuration from ${fromCwd ? 'cwd' : 'file'}:`, configFilePath) return true } private watchFile(filepath: string, target: ConfigurationTarget): void { if (!fs.existsSync(filepath) || this._watchedFiles.has(filepath) || this.noWatch) return this._watchedFiles.add(filepath) const folder = ConfigurationTarget.WorkspaceFolder ? normalizeFilePath(path.resolve(filepath, '../..')) : undefined let disposable = watchFile(filepath, () => { let model = this.parseConfigurationModel(filepath) this.changeConfiguration(target, model, folder) }) this.disposables.push(disposable) } /** * Update ConfigurationModel and fire event. */ public changeConfiguration(target: ConfigurationTarget, model: ConfigurationModel, folder: string | undefined, keys?: string[]): void { const listOnly = target === ConfigurationTarget.Default && keys && keys.every(key => key.startsWith('list.source')) let configuration = this._configuration let previous = listOnly ? undefined : configuration.toData() let change: IConfigurationChange if (target === ConfigurationTarget.Default) { change = configuration.compareAndUpdateDefaultConfiguration(model, keys) } else if (target === ConfigurationTarget.User) { change = configuration.compareAndUpdateUserConfiguration(model) } else if (target === ConfigurationTarget.Workspace) { change = configuration.compareAndUpdateWorkspaceConfiguration(model) } else if (target === ConfigurationTarget.WorkspaceFolder) { change = configuration.compareAndUpdateFolderConfiguration(folder, model) } else { change = configuration.compareAndUpdateMemoryConfiguration(model) } if (!change || change.keys.length == 0) return if ( target !== ConfigurationTarget.WorkspaceFolder, target !== ConfigurationTarget.Workspace ) { this._initialConfiguration = this.getConfiguration(undefined, null) } if (listOnly) return let ev = new ConfigurationChangeEvent(change, previous, configuration) ev.source = target this._onChange.fire(ev) } public getDefaultResource(): string | undefined { let root = this._proxy?.root if (!root) return undefined return URI.file(root).toString() } /** * Get workspace configuration */ public getConfiguration(section?: string, scope?: ConfigurationResourceScope): WorkspaceConfiguration { let configuration = this._configuration let overrides: IConfigurationOverrides = scope ? scopeToOverrides(scope) : { resource: scope === null ? undefined : this.getDefaultResource() } const config = Object.freeze(lookUp(configuration.getValue(undefined, overrides), section)) const result: WorkspaceConfiguration = { has(key: string): boolean { return typeof lookUp(config, key) !== 'undefined' }, get: (key: string, defaultValue?: T) => { let result: T = lookUp(config, key) if (result == null) return defaultValue return result }, update: (key: string, value?: any, updateTarget: ConfigurationUpdateTarget | boolean = false): Promise => { const resource = overrides.resource let entry = section ? `${section}.${key}` : key let target: ConfigurationTarget if (typeof updateTarget === 'boolean') { target = updateTarget ? ConfigurationTarget.User : ConfigurationTarget.WorkspaceFolder } else { target = convertTarget(updateTarget) } // let folderConfigFile: string | undefined let folder: string | undefined if (target === ConfigurationTarget.WorkspaceFolder) { folder = this._configuration.resolveFolder(resource) ?? this.resolveWorkspaceFolderForResource(resource) if (!folder) { console.error(`Unable to locate workspace folder configuration for ${resource}`) logger.error(`Unable to locate workspace folder configuration`, resource, Error().stack) return } } let model: ConfigurationModel = this._configuration.getConfigurationModel(target, folder).clone() if (value === undefined) { model.removeValue(entry) } else { model.setValue(entry, value) } this.changeConfiguration(target, model, folder) let fsPath: string if (target === ConfigurationTarget.WorkspaceFolder) { fsPath = this.folderToConfigfile(folder) } else if (target === ConfigurationTarget.User) { fsPath = this.userConfigFile } return fsPath ? this._proxy?.modifyConfiguration(fsPath, entry, value) : Promise.resolve() }, inspect: (key: string): ConfigurationInspect => { key = section ? `${section}.${key}` : key const config = this._configuration.inspect(key, overrides) return { key, defaultValue: config.defaultValue, globalValue: config.userValue, workspaceValue: config.workspaceValue, workspaceFolderValue: config.workspaceFolderValue } } } Object.defineProperty(result, 'has', { enumerable: false }) Object.defineProperty(result, 'get', { enumerable: false }) Object.defineProperty(result, 'update', { enumerable: false }) Object.defineProperty(result, 'inspect', { enumerable: false }) if (typeof config === 'object') { mixin(result, config, false) } return deepFreeze(result) } /** * Resolve folder configuration from uri. */ public locateFolderConfigution(uri: string): boolean { let folder = this._configuration.resolveFolder(uri) if (folder) return true let u = URI.parse(uri) if (u.scheme !== 'file') return false let dir = folder = findUp('.vim', u.fsPath) if (!dir) return false folder = path.dirname(dir) let filepath = this.folderToConfigfile(folder) if (filepath) { this.addFolderFile(filepath, false, uri) return true } return false } /** * Resolve workspace folder config file path. */ public resolveWorkspaceFolderForResource(resource?: string): string | undefined { if (this._proxy && typeof this._proxy.getWorkspaceFolder === 'function') { // fallback to check workspace folder. let uri = this._proxy.getWorkspaceFolder(resource) if (!uri) return undefined let fsPath = uri.fsPath let configFilePath = this.folderToConfigfile(fsPath) if (configFilePath) { if (!fs.existsSync(configFilePath)) { fs.mkdirSync(path.dirname(configFilePath), { recursive: true }) fs.writeFileSync(configFilePath, '{}', 'utf8') } this.addFolderFile(configFilePath, false, resource) return fsPath } } return undefined } /** * Reset configurations for test, not trigger configuration change event. */ public reset(): void { this._errors.clear() let model = new ConfigurationModel() this._configuration.updateMemoryConfiguration(model) this._initialConfiguration = this.getConfiguration(undefined, null) } public dispose(): void { this._onError.dispose() this._onChange.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/configuration/model.ts ================================================ 'use strict' import { IConfigurationModel, IOverrides, IStringDictionary } from './types' import { distinct } from '../util/array' import { objectLiteral } from '../util/is' import { deepClone, deepFreeze, equals } from '../util/object' import { addToValueTree, getConfigurationValue, removeFromValueTree } from './util' export class ConfigurationModel implements IConfigurationModel { private frozen = false private readonly overrideConfigurations = new Map() constructor( private _contents: any = {}, private readonly _keys: string[] = [], private readonly _overrides: IOverrides[] = [] ) {} public get contents(): any { return this.checkAndFreeze(this._contents) } public get overrides(): IOverrides[] { return this.checkAndFreeze(this._overrides) } public get keys(): string[] { return this.checkAndFreeze(this._keys) } public get isFrozen(): boolean { return this.frozen } private checkAndFreeze(data: T): T { if (this.frozen && !Object.isFrozen(data)) { return deepFreeze(data) } return data } public isEmpty(): boolean { return this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0 } public clone(): ConfigurationModel { return new ConfigurationModel(deepClone(this._contents), [...this.keys], deepClone(this.overrides)) } public toJSON(): IConfigurationModel { return { contents: this.contents, overrides: this.overrides, keys: this.keys } } public getValue(section?: string): V { let res = section ? getConfigurationValue(this.contents, section) : this.contents return res } public getOverrideValue(section: string | undefined, overrideIdentifier: string): V | undefined { const overrideContents = this.getContentsForOverrideIdentifier(overrideIdentifier) return overrideContents ? section ? getConfigurationValue(overrideContents, section) : overrideContents : undefined } public getKeysForOverrideIdentifier(identifier: string): string[] { const keys: string[] = [] for (const override of this.overrides) { if (override.identifiers.includes(identifier)) { keys.push(...override.keys) } } return distinct(keys) } public getAllOverrideIdentifiers(): string[] { const result: string[] = [] for (const override of this.overrides) { result.push(...override.identifiers) } return distinct(result) } public override(identifier: string): ConfigurationModel { let overrideConfigurationModel = this.overrideConfigurations.get(identifier) if (!overrideConfigurationModel) { overrideConfigurationModel = this.createOverrideConfigurationModel(identifier) this.overrideConfigurations.set(identifier, overrideConfigurationModel) } return overrideConfigurationModel } public merge(...others: ConfigurationModel[]): ConfigurationModel { const contents = deepClone(this._contents) const overrides = deepClone(this._overrides) const keys = [...this._keys] for (const other of others) { if (other.isEmpty()) { continue } this.mergeContents(contents, other.contents) for (const otherOverride of other.overrides) { const [override] = overrides.filter(o => equals(o.identifiers, otherOverride.identifiers)) if (override) { this.mergeContents(override.contents, otherOverride.contents) override.keys.push(...otherOverride.keys) override.keys = distinct(override.keys) } else { overrides.push(deepClone(otherOverride)) } } for (const key of other.keys) { if (keys.indexOf(key) === -1) { keys.push(key) } } } return new ConfigurationModel(contents, keys, overrides) } public freeze(): ConfigurationModel { this.frozen = true return this } private mergeContents(source: any, target: any): void { for (const key of Object.keys(target)) { if (key in source) { if (objectLiteral(source[key]) && objectLiteral(target[key])) { this.mergeContents(source[key], target[key]) continue } } source[key] = deepClone(target[key]) } } // Update methods public setValue(key: string, value: any) { this.addKey(key) addToValueTree(this.contents, key, value, e => { console.error(e) }) } public removeValue(key: string): void { if (this.removeKey(key)) { removeFromValueTree(this.contents, key) } } private addKey(key: string): void { let index = this.keys.length for (let i = 0; i < index; i++) { if (key.indexOf(this.keys[i]) === 0) { index = i } } this.keys.splice(index, 1, key) } private removeKey(key: string): boolean { const index = this.keys.indexOf(key) if (index !== -1) { this.keys.splice(index, 1) return true } return false } private createOverrideConfigurationModel(identifier: string): ConfigurationModel { const overrideContents = this.getContentsForOverrideIdentifier(identifier) if (!overrideContents || typeof overrideContents !== 'object' || !Object.keys(overrideContents).length) { // If there are no valid overrides, return self return this } const contents: any = {} for (const key of distinct([...Object.keys(this.contents), ...Object.keys(overrideContents)])) { let contentsForKey = this.contents[key] const overrideContentsForKey = overrideContents[key] // If there are override contents for the key, clone and merge otherwise use base contents if (overrideContentsForKey) { // Clone and merge only if base contents and override contents are of type object otherwise just override if (typeof contentsForKey === 'object' && typeof overrideContentsForKey === 'object') { contentsForKey = deepClone(contentsForKey) this.mergeContents(contentsForKey, overrideContentsForKey) } else { contentsForKey = overrideContentsForKey } } contents[key] = contentsForKey } return new ConfigurationModel(contents, this._keys, this.overrides) } private getContentsForOverrideIdentifier(identifier: string): any { let contentsForIdentifierOnly: IStringDictionary | null = null let contents: IStringDictionary | null = null const mergeContents = (contentsToMerge: any) => { if (contentsToMerge) { if (contents) { this.mergeContents(contents, contentsToMerge) } else { contents = deepClone(contentsToMerge) } } } for (const override of this.overrides) { if (equals(override.identifiers, [identifier])) { contentsForIdentifierOnly = override.contents } else if (override.identifiers.includes(identifier)) { mergeContents(override.contents) } } // Merge contents of the identifier only at the end to take precedence. mergeContents(contentsForIdentifierOnly) return contents } } ================================================ FILE: src/configuration/parser.ts ================================================ import { ParseError, ParseErrorCode, visit } from 'jsonc-parser' import { Diagnostic, Range } from 'vscode-languageserver-types' import { createLogger } from '../logger' import { ConfigurationScope, IConfigurationModel, IOverrides } from './types' import { ConfigurationModel } from './model' import { convertErrors, overrideIdentifiersFromKey, OVERRIDE_PROPERTY_REGEX, toValuesTree } from './util' const logger = createLogger('parser') export interface ConfigurationParseOptions { scopes: ConfigurationScope[] | undefined skipRestricted?: boolean } export interface ConfigurationParseError { startLine?: number startCharacter?: number length?: number message: string } export class ConfigurationModelParser { private _raw: any = null private _configurationModel: ConfigurationModel | null = null private _parseErrors: Diagnostic[] = [] constructor(protected readonly _name: string) {} public get configurationModel(): ConfigurationModel { return this._configurationModel || new ConfigurationModel() } public get errors(): Diagnostic[] { return this._parseErrors } public parse(content: string | null | undefined, options?: ConfigurationParseOptions): void { if (content != null) { const raw = this.doParseContent(content) this.parseRaw(raw, options) } } public parseRaw(raw: any, options?: ConfigurationParseOptions): void { this._raw = raw const { contents, keys, overrides } = this.doParseRaw(raw, options) this._configurationModel = new ConfigurationModel(contents, keys, overrides) // this._restrictedConfigurations = restricted || [] } private doParseContent(content: string): any { let raw: any = {} let currentProperty: string | null = null let currentParent: any = [] const previousParents: any[] = [] const _errors: ParseError[] = [] function onValue(value: any) { if (Array.isArray(currentParent)) { (currentParent).push(value) } else if (currentProperty !== null) { currentParent[currentProperty] = value } } const visitor = { onObjectBegin: () => { const object = {} onValue(object) previousParents.push(currentParent) currentParent = object currentProperty = null }, onObjectProperty: (name: string) => { currentProperty = name }, onObjectEnd: () => { currentParent = previousParents.pop() }, onArrayBegin: () => { const array: any[] = [] onValue(array) previousParents.push(currentParent) currentParent = array currentProperty = null }, onArrayEnd: () => { currentParent = previousParents.pop() }, onLiteralValue: onValue, onError: (error: ParseErrorCode, offset: number, length: number) => { _errors.push({ error, length, offset }) } } if (content) { try { visit(content, visitor, { allowTrailingComma: true, allowEmptyContent: true }) raw = currentParent[0] ?? {} if (_errors.length > 0) { this._parseErrors = convertErrors(content, _errors) } } catch (e) { this._parseErrors = [{ range: Range.create(0, 0, 0, 0), message: `Error on parse configuration file ${this._name}: ${e}` }] } } return raw } protected doParseRaw(raw: any, _options?: ConfigurationParseOptions): IConfigurationModel & { restricted?: string[] } { const onError = (message: string) => { console.error(`Conflict in settings file ${this._name}: ${message}`) } const contents = toValuesTree(raw, onError, true) const keys = Object.keys(raw) const overrides = this.toOverrides(raw, onError) return { contents, keys, overrides, restricted: [] } } private toOverrides(raw: any, conflictReporter: (message: string) => void): IOverrides[] { const overrides: IOverrides[] = [] for (const key of Object.keys(raw)) { if (OVERRIDE_PROPERTY_REGEX.test(key)) { const overrideRaw: any = {} for (const keyInOverrideRaw in raw[key]) { overrideRaw[keyInOverrideRaw] = raw[key][keyInOverrideRaw] } overrides.push({ identifiers: overrideIdentifiersFromKey(key), keys: Object.keys(overrideRaw), contents: toValuesTree(overrideRaw, conflictReporter, true) }) } } return overrides } } ================================================ FILE: src/configuration/registry.ts ================================================ import { distinct } from '../util/array' import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../util/jsonRegistry' import { IJSONSchema } from '../util/jsonSchema' import { toObject } from '../util/object' import { Emitter, Event } from '../util/protocol' import { Registry } from '../util/registry' import { ConfigurationScope, IStringDictionary } from './types' import { getDefaultValue, OVERRIDE_PROPERTY_PATTERN, OVERRIDE_PROPERTY_REGEX } from './util' const EXCLUDE_KEYS = ['log-path', 'logPath'] export const Extensions = { Configuration: 'base.contributions.configuration' } export interface IConfigurationPropertySchema extends IJSONSchema { scope?: ConfigurationScope /** * When restricted, value of this configuration will be read only from trusted sources. * For eg., If the workspace is not trusted, then the value of this configuration is not read from workspace settings file. */ restricted?: boolean /** * When `false` this property is excluded from the registry. Default is to include. */ included?: boolean /** * Labels for enumeration items */ enumItemLabels?: string[] } export interface IExtensionInfo { id: string displayName?: string } export interface IConfigurationNode { id?: string properties?: IStringDictionary scope?: ConfigurationScope extensionInfo?: IExtensionInfo } export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & { defaultDefaultValue?: any source?: IExtensionInfo // Source of the Property defaultValueSource?: IExtensionInfo | string // Source of the Default Value } export interface IConfigurationRegistry { /** * Register a configuration to the registry. */ registerConfiguration(configuration: IConfigurationNode): void /** * Register multiple configurations to the registry. */ registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): void /** * Deregister multiple configurations from the registry. */ deregisterConfigurations(configurations: IConfigurationNode[]): void /** * update the configuration registry by * - registering the configurations to add * - deregistering the configurations to remove */ updateConfigurations(configurations: { add: IConfigurationNode[]; remove: IConfigurationNode[] }): void /** * Event that fires whenever a configuration has been * registered. */ readonly onDidSchemaChange: Event /** * Event that fires whenever a configuration has been * registered. */ readonly onDidUpdateConfiguration: Event<{ properties: string[]; defaultsOverrides?: boolean }> /** * Returns all configurations settings of all configuration nodes contributed to this registry. */ getConfigurationProperties(): IStringDictionary /** * Returns all excluded configurations settings of all configuration nodes contributed to this registry. */ getExcludedConfigurationProperties(): IStringDictionary } export interface IConfigurationDefaultOverride { readonly value: any readonly source?: IExtensionInfo | string // Source of the default override readonly valuesSources?: Map // Source of each value in default language overrides } export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} } export const resourceSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} } export const resourceLanguageSettingsSchemaId = 'vscode://schemas/settings/resourceLanguage' export const configurationDefaultsSchemaId = 'vscode://schemas/settings/configurationDefaults' const contributionRegistry = Registry.as(JSONExtensions.JSONContribution) class ConfigurationRegistry implements IConfigurationRegistry { private readonly configurationProperties: IStringDictionary private readonly excludedConfigurationProperties: IStringDictionary private readonly resourceLanguageSettingsSchema: IJSONSchema private readonly _onDidSchemaChange = new Emitter() public readonly onDidSchemaChange: Event = this._onDidSchemaChange.event private readonly _onDidUpdateConfiguration = new Emitter<{ properties: string[]; defaultsOverrides?: boolean }>() public readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event constructor() { this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown coc.nvim configuration property', allowTrailingCommas: true, allowComments: true } this.configurationProperties = {} this.excludedConfigurationProperties = {} contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema) this.registerOverridePropertyPatternKey() } public registerConfiguration(configuration: IConfigurationNode, validate = true): void { this.registerConfigurations([configuration], validate) } public registerConfigurations(configurations: IConfigurationNode[], validate = true): void { const properties = this.doRegisterConfigurations(configurations, validate) contributionRegistry.notifySchemaChanged(resourceLanguageSettingsSchemaId) this._onDidSchemaChange.fire() this._onDidUpdateConfiguration.fire({ properties }) } public deregisterConfigurations(configurations: IConfigurationNode[]): void { const properties = this.doDeregisterConfigurations(configurations) contributionRegistry.notifySchemaChanged(resourceLanguageSettingsSchemaId) this._onDidSchemaChange.fire() this._onDidUpdateConfiguration.fire({ properties }) } public updateConfigurations({ add, remove }: { add: IConfigurationNode[]; remove: IConfigurationNode[] }): void { const properties = [] properties.push(...this.doDeregisterConfigurations(remove)) properties.push(...this.doRegisterConfigurations(add, false)) contributionRegistry.notifySchemaChanged(resourceLanguageSettingsSchemaId) this._onDidSchemaChange.fire() this._onDidUpdateConfiguration.fire({ properties: distinct(properties) }) } private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean): string[] { const properties: string[] = [] configurations.forEach(configuration => { properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo)) // fills in defaults this.registerJSONConfiguration(configuration) }) return properties } private doDeregisterConfigurations(configurations: IConfigurationNode[]): string[] { const properties: string[] = [] const deregisterConfiguration = (configuration: IConfigurationNode) => { for (const key in toObject(configuration.properties)) { properties.push(key) delete this.configurationProperties[key] this.removeFromSchema(key, configuration.properties[key]) } } for (const configuration of configurations) { deregisterConfiguration(configuration) } return properties } private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean, extensionInfo: IExtensionInfo | undefined, scope: ConfigurationScope = ConfigurationScope.APPLICATION): string[] { scope = configuration.scope == null ? scope : configuration.scope const propertyKeys: string[] = [] const properties = configuration.properties for (const key in toObject(properties)) { const property: IRegisteredConfigurationPropertySchema = properties[key] if (validate && validateProperty(key, property)) { delete properties[key] continue } property.source = extensionInfo // update default value property.defaultDefaultValue = properties[key].default this.updatePropertyDefaultValue(key, property) // update scope property.scope = property.scope == null ? scope : property.scope if (extensionInfo) property.description = (property.description ? `${property.description}\n` : '') + `From ${extensionInfo.id}` // Add to properties maps // Property is included by default if 'included' is unspecified if (property.hasOwnProperty('included') && !property.included) { this.excludedConfigurationProperties[key] = properties[key] delete properties[key] continue } else { this.configurationProperties[key] = properties[key] } if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) { // If not set, default deprecationMessage to the markdown source properties[key].deprecationMessage = properties[key].markdownDeprecationMessage } propertyKeys.push(key) } return propertyKeys } public getConfigurationProperties(): IStringDictionary { return this.configurationProperties } public getExcludedConfigurationProperties(): IStringDictionary { return this.excludedConfigurationProperties } private registerJSONConfiguration(configuration: IConfigurationNode) { const register = (configuration: IConfigurationNode) => { const properties = configuration.properties for (const key in toObject(properties)) { this.updateSchema(key, properties[key]) } } register(configuration) } private updateSchema(key: string, property: IConfigurationPropertySchema): void { allSettings.properties[key] = property switch (property.scope) { case ConfigurationScope.WINDOW: case ConfigurationScope.RESOURCE: resourceSettings.properties[key] = property break case ConfigurationScope.LANGUAGE_OVERRIDABLE: resourceSettings.properties[key] = property this.resourceLanguageSettingsSchema.properties![key] = property break } } private removeFromSchema(key: string, property: IConfigurationPropertySchema): void { delete allSettings.properties[key] switch (property.scope) { case ConfigurationScope.WINDOW: case ConfigurationScope.RESOURCE: case ConfigurationScope.LANGUAGE_OVERRIDABLE: delete resourceSettings.properties[key] delete this.resourceLanguageSettingsSchema.properties![key] break } } private registerOverridePropertyPatternKey(): void { const resourceLanguagePropertiesSchema: IJSONSchema = { type: 'object', description: 'Configure editor settings to be overridden for a language.', errorMessage: 'This setting does not support per-language configuration.', $ref: resourceLanguageSettingsSchemaId, } allSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema resourceSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema } private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void { let defaultValue = property.defaultDefaultValue if (typeof defaultValue === 'undefined' && !EXCLUDE_KEYS.some(k => key.includes(k))) { defaultValue = getDefaultValue(property.type) } property.default = defaultValue property.defaultValueSource = undefined } } const configurationRegistry = new ConfigurationRegistry() Registry.add(Extensions.Configuration, configurationRegistry) export function validateProperty(property: string, _schema: IRegisteredConfigurationPropertySchema = undefined): string | null { if (!property.trim()) { return 'Cannot register an empty property' } if (OVERRIDE_PROPERTY_REGEX.test(property)) { return `Cannot register ${property}. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings` } if (configurationRegistry.getConfigurationProperties()[property] !== undefined) { return `Cannot register '${property}'. This property is already registered.` } return null } ================================================ FILE: src/configuration/shape.ts ================================================ 'use strict' import { applyEdits, modify } from 'jsonc-parser' import { WorkspaceFolder } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { createLogger } from '../logger' import { fs, path } from '../util/node' const logger = createLogger('configuration-shape') interface IFolderController { root?: string getWorkspaceFolder?: (resource: string) => WorkspaceFolder } export interface IConfigurationShape { root?: string /** * Resolve possible workspace config from resource. */ getWorkspaceFolder?(resource?: string): URI | undefined modifyConfiguration(fsPath: string, key: string, value?: any): Promise } export default class ConfigurationProxy implements IConfigurationShape { constructor(private resolver: IFolderController, private _test = global.__TEST__) { } public get root(): string | undefined { return this.resolver.root } public async modifyConfiguration(fsPath: string, key: string, value?: any): Promise { if (this._test) return logger.info(`modify configuration file: ${fsPath}`, key, value) let dir = path.dirname(fsPath) let formattingOptions = { tabSize: 2, insertSpaces: true } if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) let content = fs.readFileSync(fsPath, { flag: 'a+' }).toString('utf8') content = content || '{}' let edits = modify(content, [key], value, { formattingOptions }) content = applyEdits(content, edits) fs.writeFileSync(fsPath, content, { encoding: 'utf8' }) } public getWorkspaceFolder(resource: string): URI | undefined { if (typeof this.resolver.getWorkspaceFolder === 'function') { let workspaceFolder = this.resolver.getWorkspaceFolder(resource) if (workspaceFolder) return URI.parse(workspaceFolder.uri) } return undefined } } ================================================ FILE: src/configuration/types.ts ================================================ import type { TextDocument } from 'vscode-languageserver-textdocument' import type { WorkspaceFolder } from 'vscode-languageserver-types' import type { URI } from 'vscode-uri' /** * An interface for a JavaScript object that * acts a dictionary. The keys are strings. */ export type IStringDictionary = Record export enum ConfigurationTarget { Default, User, Workspace, WorkspaceFolder, Memory, } export interface IConfigurationChange { keys: string[] overrides: [string, string[]][] } export enum ConfigurationUpdateTarget { Global = 1, Workspace = 2, WorkspaceFolder = 3 } export const enum ConfigurationScope { /** * Application specific configuration, which can be configured only in local user settings. */ APPLICATION = 1, /** * Window specific configuration, which can be configured in the user or workspace settings. */ WINDOW, /** * Resource specific configuration, which can be configured in the user, workspace or folder settings. */ RESOURCE, /** * Resource specific configuration that can be configured in language specific settings */ LANGUAGE_OVERRIDABLE, } export type ConfigurationResourceScope = string | null | URI | TextDocument | WorkspaceFolder | { uri?: string; languageId?: string } export interface IConfigurationChangeEvent { readonly source: ConfigurationTarget readonly affectedKeys: string[] readonly change?: IConfigurationChange affectsConfiguration(configuration: string, scope?: ConfigurationResourceScope): boolean } export interface ConfigurationInspect { key: string defaultValue?: T globalValue?: T workspaceValue?: T workspaceFolderValue?: T } export interface IConfigurationOverrides { overrideIdentifier?: string | null resource?: string | null } export interface IOverrides { contents: any keys: string[] identifiers: string[] } export interface IConfigurationModel { contents: any keys: string[] overrides: IOverrides[] } export interface IConfigurationData { defaults: IConfigurationModel memory: IConfigurationModel user: IConfigurationModel workspace: IConfigurationModel folders: [string, IConfigurationModel][] } export interface WorkspaceConfiguration { /** * Return a value from this configuration. * @param section Configuration name, supports _dotted_ names. * @return The value `section` denotes or `undefined`. */ get(section: string): T | undefined /** * Return a value from this configuration. * @param section Configuration name, supports _dotted_ names. * @param defaultValue A value should be returned when no value could be found, is `undefined`. * @return The value `section` denotes or the default. */ get(section: string, defaultValue: T): T /** * Check if this configuration has a certain value. * @param section Configuration name, supports _dotted_ names. * @return `true` if the section doesn't resolve to `undefined`. */ has(section: string): boolean /** * Retrieve all information about a configuration setting. A configuration value * often consists of a *default* value, a global or installation-wide value, * a workspace-specific value * * *Note:* The configuration name must denote a leaf in the configuration tree * (`editor.fontSize` vs `editor`) otherwise no result is returned. * @param section Configuration name, supports _dotted_ names. * @return Information about a configuration setting or `undefined`. */ inspect(section: string): ConfigurationInspect | undefined /** * Update a configuration value. The updated configuration values are persisted. * @param section Configuration name, supports _dotted_ names. * @param value The new value. * @param isUser if true, always update user configuration */ update(section: string, value: any, isUser?: ConfigurationUpdateTarget | boolean): Thenable /** * Readable dictionary that backs this configuration. */ readonly [key: string]: any } ================================================ FILE: src/configuration/util.ts ================================================ 'use strict' import { ParseError, printParseErrorCode } from 'jsonc-parser' import { TextDocument } from 'vscode-languageserver-textdocument' import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { distinct } from '../util/array' import * as Is from '../util/is' import { os } from '../util/node' import { equals, hasOwnProperty } from '../util/object' import { ConfigurationResourceScope, ConfigurationTarget, ConfigurationUpdateTarget, IConfigurationChange, IConfigurationOverrides } from './types' const documentUri = 'file:///1' export interface IConfigurationCompareResult { added: string[] removed: string[] updated: string[] overrides: [string, string[]][] } const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]` const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g') export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$` export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN) /** * Basic expand for ${env:value}, ${cwd}, ${userHome} */ export function expand(input: string): string { return input.replace(/\$\{(.*?)\}/g, (match: string, name: string) => { if (name.startsWith('env:')) { let key = name.split(':')[1] return process.env[key] ?? match } switch (name) { case 'tmpdir': return os.tmpdir() case 'userHome': return os.homedir() case 'cwd': return process.cwd() default: return match } }) } export function expandObject(obj: any): any { if (obj == null) return obj if (typeof obj === 'string') return expand(obj) if (Array.isArray(obj)) return obj.map(obj => expandObject(obj)) if (Is.objectLiteral(obj)) { for (let key of Object.keys(obj)) { obj[key] = expandObject(obj[key]) } return obj } return obj } export function convertTarget(updateTarget: ConfigurationUpdateTarget): ConfigurationTarget { let target: ConfigurationTarget switch (updateTarget) { case ConfigurationUpdateTarget.Global: target = ConfigurationTarget.User break case ConfigurationUpdateTarget.Workspace: target = ConfigurationTarget.Workspace break default: target = ConfigurationTarget.WorkspaceFolder } return target } export function scopeToOverrides(scope: ConfigurationResourceScope): IConfigurationOverrides { let overrides: IConfigurationOverrides if (typeof scope === 'string') { overrides = { resource: scope } } else if (URI.isUri(scope)) { overrides = { resource: scope.toString() } } else if (scope != null) { let uri = scope['uri'] let languageId = scope['languageId'] overrides = { resource: uri, overrideIdentifier: languageId } } return overrides } export function overrideIdentifiersFromKey(key: string): string[] { const identifiers: string[] = [] if (OVERRIDE_PROPERTY_REGEX.test(key)) { let matches = OVERRIDE_IDENTIFIER_REGEX.exec(key) while (matches?.length) { const identifier = matches[1].trim() if (identifier) { identifiers.push(identifier) } matches = OVERRIDE_IDENTIFIER_REGEX.exec(key) } } return distinct(identifiers) } function getOrSet(map: Map, key: K, value: V): V { let result = map.get(key) if (result === undefined) { result = value map.set(key, result) } return result } export function mergeChanges(...changes: IConfigurationChange[]): IConfigurationChange { if (changes.length === 0) { return { keys: [], overrides: [] } } if (changes.length === 1) { return changes[0] } const keysSet = new Set() const overridesMap = new Map>() for (const change of changes) { change.keys.forEach(key => keysSet.add(key)) change.overrides.forEach(([identifier, keys]) => { const result = getOrSet(overridesMap, identifier, new Set()) keys.forEach(key => result.add(key)) }) } const overrides: [string, string[]][] = [] overridesMap.forEach((keys, identifier) => overrides.push([identifier, [...keys.values()]])) return { keys: [...keysSet.values()], overrides } } export function mergeConfigProperties(obj: any): any { let res = {} for (let key of Object.keys(obj)) { if (key.indexOf('.') == -1) { res[key] = obj[key] } else { let parts = key.split('.') let pre = res let len = parts.length for (let i = 0; i < len; i++) { let k = parts[i] if (i == len - 1) { pre[k] = obj[key] } else { pre[k] = pre[k] || {} pre = pre[k] } } } } return res } export function convertErrors(content: string, errors: ParseError[]): Diagnostic[] { let items: Diagnostic[] = [] let document = TextDocument.create(documentUri, 'json', 0, content) for (let err of errors) { const range = Range.create(document.positionAt(err.offset), document.positionAt(err.offset + err.length)) items.push(Diagnostic.create(range, printParseErrorCode(err.error), DiagnosticSeverity.Error)) } return items } export function toValuesTree(properties: { [qualifiedKey: string]: any }, conflictReporter: (message: string) => void, doExpand = false): any { const root = Object.create(null) for (const key in properties) { addToValueTree(root, key, properties[key], conflictReporter, doExpand) } return root } export function addToValueTree(settingsTreeRoot: any, key: string, value: any, conflictReporter: (message: string) => void, doExpand = false): void { const segments = key.split('.') const last = segments.pop()! let curr = settingsTreeRoot for (let i = 0; i < segments.length; i++) { const s = segments[i] let obj = curr[s] switch (typeof obj) { case 'undefined': obj = curr[s] = Object.create(null) break case 'object': break default: if (conflictReporter) conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`) return } curr = obj } if (typeof curr === 'object' && curr !== null) { if (doExpand) { curr[last] = expandObject(value) } else { curr[last] = value } } else { if (conflictReporter) conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`) } } export function removeFromValueTree(valueTree: any, key: string): void { const segments = key.split('.') doRemoveFromValueTree(valueTree, segments) } function doRemoveFromValueTree(valueTree: any, segments: string[]): void { const first = segments.shift() if (segments.length === 0) { // Reached last segment delete valueTree[first] return } if (Object.keys(valueTree).includes(first)) { const value = valueTree[first] if (typeof value === 'object' && !Array.isArray(value)) { doRemoveFromValueTree(value, segments) if (Object.keys(value).length === 0) { delete valueTree[first] } } } } export function getConfigurationValue( config: any, settingPath: string, defaultValue?: T ): T { function accessSetting(config: any, path: string[]): any { let current = config for (let i = 0; i < path.length; i++) { if (typeof current !== 'object' || current === null) { return undefined } current = current[path[i]] } return current as T } const path = settingPath.split('.') const result = accessSetting(config, path) return typeof result === 'undefined' ? defaultValue : result } export function toJSONObject(obj: any): any { if (obj) { if (Array.isArray(obj)) { return obj.map(toJSONObject) } else if (typeof obj === 'object') { const res = Object.create(null) for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { res[key] = toJSONObject(obj[key]) } } return res } } return obj } /** * Compare too configuration contents */ export function compareConfigurationContents(to: { keys: string[]; contents: any } | undefined, from: { keys: string[]; contents: any } | undefined) { const added = to ? from ? to.keys.filter(key => from.keys.indexOf(key) === -1) : [...to.keys] : [] const removed = from ? to ? from.keys.filter(key => to.keys.indexOf(key) === -1) : [...from.keys] : [] const updated: string[] = [] if (to && from) { for (const key of from.keys) { if (to.keys.indexOf(key) !== -1) { const value1 = getConfigurationValue(from.contents, key) const value2 = getConfigurationValue(to.contents, key) if (!equals(value1, value2)) { updated.push(key) } } } } return { added, removed, updated } } export function getDefaultValue(type: string | string[] | undefined): any { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const t = Array.isArray(type) ? (type)[0] : type switch (t) { case 'boolean': return false case 'integer': case 'number': return 0 case 'string': return '' case 'array': return [] case 'object': return {} default: return null } } export function lookUp(tree: any, key: string): any { if (key) { if (tree && hasOwnProperty(tree, key)) return tree[key] const parts = key.split('.') let node = tree for (let i = 0; node && i < parts.length; i++) { node = node[parts[i]] } return node } return tree } ================================================ FILE: src/core/autocmds.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { createLogger } from '../logger' import { Autocmd } from '../types' import { isFalsyOrEmpty } from '../util/array' import { parseExtensionName } from '../util/extensionRegistry' import { omit } from '../util/lodash' import { CancellationTokenSource, Disposable } from '../util/protocol' const logger = createLogger('autocmds') interface AutocmdOption { group?: string | number pattern?: string | string[] buffer?: number desc?: string command?: string once?: boolean nested?: boolean replace?: boolean } export interface AutocmdOptionWithStack extends Autocmd { stack: string } export class AutocmdItem { private _extensiionName: string | undefined constructor( public readonly id: number, public readonly option: AutocmdOptionWithStack ) { } public get extensiionName(): string { if (this._extensiionName) return this._extensiionName this._extensiionName = parseExtensionName(this.option.stack) return this._extensiionName } } const groupName = 'coc_dynamic_autocmd' export function toAutocmdOption(item: AutocmdItem): AutocmdOption { let { id, option } = item let opt: AutocmdOption = { group: groupName } if (option.buffer) opt.buffer = option.buffer if (option.pattern) opt.pattern = option.pattern if (option.once) opt.once = true if (option.nested) opt.nested = true let method = option.request ? 'request' : 'notify' let args = isFalsyOrEmpty(option.arglist) ? '' : ', ' + option.arglist.join(', ') let command = `call coc#rpc#${method}('doAutocmd', [${id}${args}])` opt.command = command return opt } export default class Autocmds implements Disposable { public readonly autocmds: Map = new Map() private nvim: Neovim private id = 0 public attach(nvim: Neovim): void { this.nvim = nvim } public async doAutocmd(id: number, args: any[], timeout = 1000): Promise { let autocmd = this.autocmds.get(id) if (autocmd) { let option = autocmd.option logger.trace(`Invoke autocmd from "${autocmd.extensiionName}"`, option) try { let tokenSource = new CancellationTokenSource() let promise = Promise.resolve(option.callback.apply(option.thisArg, [...args, tokenSource.token])) if (option.request) { let timer let tp = new Promise(resolve => { timer = setTimeout(() => { tokenSource.cancel() logger.error(`Autocmd timeout after ${timeout}ms`, omit(option, ['callback', 'stack']), autocmd.option.stack) resolve(undefined) }, timeout) }) await Promise.race([tp, promise]) clearTimeout(timer) tokenSource.dispose() } else { await promise } } catch (e) { logger.error(`Error on autocmd "${option.event}"`, omit(option, ['callback', 'stack']), e) } } } public registerAutocmd(autocmd: AutocmdOptionWithStack): Disposable { // Used as group name as well let id = ++this.id let item = new AutocmdItem(id, autocmd) this.autocmds.set(id, item) this.createAutocmd(item) return Disposable.create(() => { // only remove the item from autocmds this.autocmds.delete(id) }) } private createAutocmd(item: AutocmdItem): void { let { option } = item let event = Array.isArray(option.event) ? option.event.join(',') : option.event if (/\buser\b/i.test(event)) { let cmd = createCommand(item.id, event, option) this.nvim.command(cmd, true) } else { let opt = toAutocmdOption(item) this.nvim.createAutocmd( Array.isArray(item.option.event) ? item.option.event : item.option.event.split(","), opt, true ) } } public removeExtensionAutocmds(extensiionName: string): void { let { nvim, autocmds } = this nvim.pauseNotification() nvim.command(`autocmd! ${groupName}`, true) let items = autocmds.values() for (const item of items) { if (item.extensiionName === extensiionName) { autocmds.delete(item.id) continue } this.createAutocmd(item) } nvim.resumeNotification(false, true) } public dispose(): void { this.autocmds.clear() } } /** * Only used for user autocmd, which can't be used for nvim_create_autocmd */ export function createCommand(id: number, event: string, autocmd: Autocmd): string { let args = isFalsyOrEmpty(autocmd.arglist) ? '' : ', ' + autocmd.arglist.join(', ') let method = autocmd.request ? 'request' : 'notify' let opt = '' if (autocmd.once) opt += ' ++once' if (autocmd.nested) opt += ' ++nested' return `autocmd ${groupName} ${event}${opt} call coc#rpc#${method}('doAutocmd', [${id}${args}])` } ================================================ FILE: src/core/channels.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { URI } from 'vscode-uri' import events from '../events' import BufferChannel from '../model/outputChannel' import { TextDocumentContentProvider } from '../provider' import { OutputChannel } from '../types' import { Disposable } from '../util/protocol' class Channels { private outputChannels: Map = new Map() private bufnrs: Map = new Map() private disposable: Disposable constructor() { this.disposable = events.on('BufUnload', bufnr => { let name = this.bufnrs.get(bufnr) if (name) { let channel = this.outputChannels.get(name) if (channel) channel.created = false } }) } /** * Get text document provider */ public getProvider(nvim: Neovim): TextDocumentContentProvider { let provider: TextDocumentContentProvider = { onDidChange: null, provideTextDocumentContent: async (uri: URI) => { let channel = this.get(uri.path.slice(1)) if (!channel) return '' nvim.pauseNotification() nvim.call('bufnr', ['%'], true) nvim.command('setlocal nospell nofoldenable nowrap noswapfile', true) nvim.command('setlocal buftype=nofile bufhidden=hide', true) nvim.command('setfiletype log', true) let res = await nvim.resumeNotification() this.bufnrs.set(res[0][0] as number, channel.name) channel.created = true return channel.content } } return provider } public get names(): string[] { return Array.from(this.outputChannels.keys()) } public get(channelName: string): BufferChannel | null { return this.outputChannels.get(channelName) } public create(name: string, nvim: Neovim): OutputChannel | null { if (this.outputChannels.has(name)) return this.outputChannels.get(name) let channel = new BufferChannel(name, nvim, () => { this.outputChannels.delete(name) }) this.outputChannels.set(name, channel) return channel } public show(name: string, cmd: string, preserveFocus?: boolean): void { let channel = this.outputChannels.get(name) if (!channel) return channel.show(preserveFocus, cmd) } public dispose(): void { this.disposable.dispose() for (let channel of this.outputChannels.values()) { channel.dispose() } this.outputChannels.clear() } } export default new Channels() ================================================ FILE: src/core/contentProvider.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { URI } from 'vscode-uri' import events from '../events' import { TextDocumentContentProvider } from '../provider' import { disposeAll } from '../util' import { CancellationTokenSource, Disposable, Emitter, Event } from '../util/protocol' import { toText } from '../util/string' import Documents from './documents' export default class ContentProvider implements Disposable { private nvim: Neovim private disposables: Disposable[] = [] private providers: Map = new Map() private readonly _onDidProviderChange = new Emitter() public readonly onDidProviderChange: Event = this._onDidProviderChange.event constructor( private documents: Documents ) { } public attach(nvim: Neovim): void { this.nvim = nvim events.on('BufReadCmd', this.onBufReadCmd, this, this.disposables) } public get schemes(): string[] { return Array.from(this.providers.keys()) } public async onBufReadCmd(scheme: string, uri: string): Promise { let provider = this.providers.get(scheme) if (!provider) return let tokenSource = new CancellationTokenSource() let content = await Promise.resolve(provider.provideTextDocumentContent(URI.parse(uri), tokenSource.token)) let buf = await this.nvim.buffer await buf.setLines(toText(content).split(/\r?\n/), { start: 0, end: -1, strictIndexing: false }) process.nextTick(() => { void events.fire('BufCreate', [buf.id]) }) } private resetAutocmds(): void { let { nvim, schemes } = this nvim.pauseNotification() nvim.command(`autocmd! coc_dynamic_content`, true) for (let scheme of schemes) { nvim.command(getAutocmdCommand(scheme), true) } nvim.resumeNotification(false, true) } public registerTextDocumentContentProvider(scheme: string, provider: TextDocumentContentProvider): Disposable { this.providers.set(scheme, provider) this._onDidProviderChange.fire() let disposables: Disposable[] = [] if (provider.onDidChange) { provider.onDidChange(async uri => { let doc = this.documents.getDocument(uri.toString()) if (!doc) return let tokenSource = new CancellationTokenSource() let content = await Promise.resolve(provider.provideTextDocumentContent(uri, tokenSource.token)) await doc.buffer.setLines(content.split(/\r?\n/), { start: 0, end: -1, strictIndexing: false }) }, null, disposables) } this.nvim.command(getAutocmdCommand(scheme), true) return Disposable.create(() => { this.providers.delete(scheme) disposeAll(disposables) this.resetAutocmds() this._onDidProviderChange.fire() }) } public dispose(): void { disposeAll(this.disposables) this._onDidProviderChange.dispose() this.providers.clear() } } function getAutocmdCommand(scheme: string): string { let rhs = `call coc#rpc#request('CocAutocmd', ['BufReadCmd','${scheme}', expand('')]) | filetype detect` return `autocmd! coc_dynamic_content BufReadCmd,FileReadCmd,SourceCmd ${scheme}:/* ${rhs}` } ================================================ FILE: src/core/dialogs.ts ================================================ import type { Neovim } from '@chemzqm/neovim' import type { WorkspaceConfiguration } from '../configuration/types' import events from '../events' import { Dialog, DialogConfig, DialogPreferences } from '../model/dialog' import InputBox, { InputPreference } from '../model/input' import Menu, { MenuItem } from '../model/menu' import Picker, { toPickerItems } from '../model/picker' import QuickPick from '../model/quickpick' import { Env, QuickPickItem } from '../types' import { defaultValue } from '../util' import { isFalsyOrEmpty } from '../util/array' import { floatHighlightGroup } from '../util/constants' import { Mutex } from '../util/mutex' import { toNumber } from '../util/numbers' import { isWindows } from '../util/platform' import { CancellationToken } from '../util/protocol' import { toText } from '../util/string' import { callAsync } from './funcs' import { showPrompt } from './ui' export type Item = QuickPickItem | string export type InputOptions = Pick export interface QuickPickConfig { placeholder?: string title?: string items?: readonly T[] value?: string canSelectMany?: boolean matchOnDescription?: boolean } /** * Options to configure the behavior of the quick pick UI. */ export interface QuickPickOptions { placeHolder?: string /** * An optional string that represents the title of the quick pick. */ title?: string /** * An optional flag to include the description when filtering the picks. */ matchOnDescription?: boolean /** * An optional flag to make the picker accept multiple selections, if true the result is an array of picks. */ canPickMany?: boolean } export type MenuOption = { title?: string, content?: string /** * Create and highlight shortcut characters. */ shortcuts?: boolean /** * Position of menu picker, default to 'cursor' */ position?: 'cursor' | 'center' /** * Border highlight that override user configuration. */ borderhighlight?: string } | string export class Dialogs { public mutex = new Mutex() public nvim: Neovim public configuration: WorkspaceConfiguration constructor() { } public async showDialog(config: DialogConfig): Promise { return await this.mutex.use(async () => { let dialog = new Dialog(this.nvim, config) await dialog.show(this.dialogPreference) return dialog }) } public async showPrompt(title: string): Promise { return await this.mutex.use(() => { return showPrompt(this.nvim, title) }) } public async createQuickPick(config: QuickPickConfig): Promise> { return await this.mutex.use(async () => { let quickpick = new QuickPick(this.nvim, this.dialogPreference) Object.assign(quickpick, config) return quickpick }) } public async showMenuPicker(items: string[] | MenuItem[], option?: MenuOption, token?: CancellationToken): Promise { return await this.mutex.use(async () => { if (token && token.isCancellationRequested) return -1 option = option || {} if (typeof option === 'string') option = { title: option } let menu = new Menu(this.nvim, { items, ...option }, token) let promise = new Promise(resolve => { menu.onDidClose(selected => { void events.race(['BufHidden'], 20).finally(() => { resolve(selected) }) }) }) await menu.show(this.dialogPreference) return await promise }) } /** * Shows a selection list. */ public async showQuickPick(itemsOrItemsPromise: Item[] | Promise, options: QuickPickOptions, token: CancellationToken): Promise { options = defaultValue(options, {}) const items = await Promise.resolve(itemsOrItemsPromise) if (isFalsyOrEmpty(items)) return undefined let isText = items.some(s => typeof s === 'string') return await this.mutex.use(() => { return new Promise((resolve, reject) => { if (token.isCancellationRequested) return resolve(undefined) let quickpick = new QuickPick(this.nvim, this.dialogPreference) quickpick.items = items.map(o => typeof o === 'string' ? { label: o } : o) quickpick.title = toText(options.title) quickpick.placeholder = options.placeHolder ?? options['placeholder'] quickpick.canSelectMany = !!options.canPickMany quickpick.matchOnDescription = options.matchOnDescription quickpick.onDidFinish(items => { if (items == null) return resolve(undefined) let arr = isText ? items.map(o => o.label) : items if (options.canPickMany) return resolve(arr) resolve(arr[0]) }) quickpick.show().catch(reject) }) }) } public async showPickerDialog(items: string[], title: string, token?: CancellationToken): Promise public async showPickerDialog(items: T[], title: string, token?: CancellationToken): Promise public async showPickerDialog(items: any, title: string, token?: CancellationToken): Promise { return await this.mutex.use(async () => { if (token && token.isCancellationRequested) { return undefined } const picker = new Picker(this.nvim, { title, items: toPickerItems(items), }, token) let promise = new Promise(resolve => { picker.onDidClose(selected => { resolve(selected) }) }) await picker.show(this.dialogPreference) let picked = await promise return picked == undefined ? undefined : items.filter((_, i) => picked.includes(i)) }) } public async requestInput(title: string, env: Env, value?: string, option?: InputOptions): Promise { let { nvim } = this let noPrompt = !env.terminal || !env.dialog || (env.isVim && isWindows && !env.isCygwin) const promptInput = this.configuration.get('coc.preferences.promptInput') if (promptInput && !noPrompt) { return await this.mutex.use(async () => { let input = new InputBox(nvim, toText(value)) await input.show(title, Object.assign(this.inputPreference, defaultValue(option, {}))) return await new Promise(resolve => { input.onDidFinish(text => { setTimeout(() => { resolve(text) }, 20) }) }) }) } else { let res = await callAsync(this.nvim, 'input', [title + ': ', toText(value)]) nvim.command('normal! :', true) return res } } /** * Request selection by use inputlist of vim. */ public async requestInputList(prompt: string, items: string[]): Promise { let { nvim } = this let list = items.map((text, i) => `${i + 1}. ${text}`) let res = await callAsync(this.nvim, 'inputlist', [[`${prompt}:`, ...list]]) nvim.command('normal! :', true) return res >= 1 && res <= items.length ? res - 1 : -1 } public async createInputBox(title: string, value: string | undefined, option?: InputPreference): Promise { let input = new InputBox(this.nvim, toText(value)) await input.show(title, Object.assign(this.inputPreference, defaultValue(option, {}))) return input } private get inputPreference(): InputPreference { let config = this.configuration.get('dialog') return { rounded: !!config.rounded, maxWidth: toNumber(config.maxWidth, 80), highlight: defaultValue(config.floatHighlight, floatHighlightGroup), borderhighlight: defaultValue(config.floatBorderHighlight, floatHighlightGroup) } } private get dialogPreference(): DialogPreferences { let config = this.configuration.get('dialog') return { rounded: !!config.rounded, maxWidth: toNumber(config.maxWidth, 80), maxHeight: config.maxHeight, floatHighlight: defaultValue(config.floatHighlight, floatHighlightGroup), floatBorderHighlight: defaultValue(config.floatBorderHighlight, floatHighlightGroup), pickerButtons: config.pickerButtons, pickerButtonShortcut: config.pickerButtonShortcut, confirmKey: toText(config.confirmKey), shortcutHighlight: toText(config.shortcutHighlight) } } } ================================================ FILE: src/core/documents.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { FormattingOptions, Location, LocationLink, TextEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import Configurations from '../configuration' import { IConfigurationChangeEvent } from '../configuration/types' import events from '../events' import languages from '../languages' import { createLogger } from '../logger' import Document from '../model/document' import { LinesTextDocument } from '../model/textdocument' import { BufferOption, DidChangeTextDocumentParams, Env, LocationWithTarget, QuickfixItem } from '../types' import { defaultValue, disposeAll, getConditionValue } from '../util' import { isFalsyOrEmpty } from '../util/array' import { isVim } from '../util/constants' import { convertFormatOptions, VimFormatOption } from '../util/convert' import { normalizeFilePath, readFile, readFileLine, resolveRoot } from '../util/fs' import { emptyObject } from '../util/is' import { fs, os, path } from '../util/node' import { hasOwnProperty, toObject } from '../util/object' import * as platform from '../util/platform' import { CancellationTokenSource, Disposable, Emitter, Event, TextDocumentSaveReason } from '../util/protocol' import { byteIndex, toText } from '../util/string' import type { TextDocumentWillSaveEvent } from './files' import WorkspaceFolder from './workspaceFolder' const logger = createLogger('core-documents') interface StateInfo { bufnr: number winid: number bufnrs: number[] winids: number[] } interface DocumentsConfig { maxFileSize: number willSaveHandlerTimeout: number formatOnSaveTimeout: number useQuickfixForLocations: boolean } const cwd = normalizeFilePath(process.cwd()) // Many FileType events may emitted on buffer reload const filetypeDelay = getConditionValue(50, 10) export default class Documents implements Disposable { private _cwd: string private _env: Env private _bufnr: number private _root: string private _attached = false private _currentResolve = false private nvim: Neovim private config: DocumentsConfig private disposables: Disposable[] = [] private _filetypeTimer: Map = new Map() private creating: Map> = new Map() public buffers: Map = new Map() private resolves: ((doc: Document) => void)[] = [] private readonly _onDidOpenTextDocument = new Emitter() private readonly _onDidCloseDocument = new Emitter() private readonly _onDidChangeDocument = new Emitter() private readonly _onDidSaveDocument = new Emitter() private readonly _onWillSaveDocument = new Emitter() public readonly onDidOpenTextDocument: Event = this._onDidOpenTextDocument.event public readonly onDidCloseDocument: Event = this._onDidCloseDocument.event public readonly onDidChangeDocument: Event = this._onDidChangeDocument.event public readonly onDidSaveTextDocument: Event = this._onDidSaveDocument.event public readonly onWillSaveTextDocument: Event = this._onWillSaveDocument.event constructor( private readonly configurations: Configurations, private readonly workspaceFolder: WorkspaceFolder, ) { this._cwd = cwd this.getConfiguration() this.configurations.onDidChange(this.getConfiguration, this, this.disposables) } public async attach(nvim: Neovim, env: Env): Promise { if (this._attached) return this.nvim = nvim this._env = env this._attached = true let { bufnrs, bufnr } = await this.nvim.call('coc#util#all_state') as StateInfo this._bufnr = bufnr await Promise.all(bufnrs.map(bufnr => this.createDocument(bufnr))) if (isVim) { const checkedTick: Map = new Map() events.on('CursorHold', async bufnr => { let doc = this.getDocument(bufnr) if (doc && doc.attached && checkedTick.get(bufnr) != doc.changedtick) { let sha256 = doc.getSha256() let same = await nvim.callVim('coc#vim9#Check_sha256', [bufnr, sha256]) checkedTick.set(bufnr, doc.changedtick) if (!same) await doc.fetchLines() } }, null, this.disposables) } events.on('BufDetach', this.onBufDetach, this, this.disposables) events.on('BufRename', async bufnr => { this.detachBuffer(bufnr) await this.createDocument(bufnr) }, null, this.disposables) events.on('DirChanged', cwd => { this._cwd = normalizeFilePath(cwd) }, null, this.disposables) const checkCurrentBuffer = (bufnr: number) => { this._bufnr = bufnr void this.createDocument(bufnr) } events.on('CursorMoved', checkCurrentBuffer, null, this.disposables) events.on('CursorMovedI', checkCurrentBuffer, null, this.disposables) events.on('BufUnload', this.onBufUnload, this, this.disposables) events.on('BufEnter', this.onBufEnter, this, this.disposables) events.on('BufCreate', this.onBufCreate, this, this.disposables) events.on('TermOpen', this.onBufCreate, this, this.disposables) events.on('BufWritePost', this.onBufWritePost, this, this.disposables) events.on('BufWritePre', this.onBufWritePre, this, this.disposables) events.on('FileType', this.onFileTypeChange, this, this.disposables) events.on('BufEnter', (bufnr: number) => { void this.createDocument(bufnr) }, null, this.disposables) events.on('TextChanged', this.onTextChange, this, this.disposables) events.on('TextChangedI', this.onTextChange, this, this.disposables) } private onTextChange(bufnr: number): void { let doc = this.getDocument(bufnr) if (doc) doc.onTextChange() } private getConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('coc.preferences')) { let config = this.configurations.initialConfiguration.get('coc.preferences') as any const bytes = require('bytes') this.config = { maxFileSize: bytes.parse(config.maxFileSize), willSaveHandlerTimeout: defaultValue(config.willSaveHandlerTimeout, 500), formatOnSaveTimeout: defaultValue(config.formatOnSaveTimeout, 500), useQuickfixForLocations: config.useQuickfixForLocations } } } public get bufnr(): number { return this._bufnr } public get root(): string { return this._root } public get cwd(): string { return this._cwd } public get documents(): Document[] { return Array.from(this.buffers.values()).filter(o => o.attached) } public async getCurrentUri(): Promise { let bufnr = await this.nvim.call('bufnr', ['%']) as number let doc = this.getDocument(bufnr) return doc ? doc.uri : undefined } public *attached(schema?: string): Iterable { for (let doc of this.buffers.values()) { if (!doc.attached) continue if (schema && doc.schema !== schema) continue yield doc } } public get bufnrs(): Iterable { return this.buffers.keys() } public detach(): void { this._attached = false for (let bufnr of this.buffers.keys()) { this.onBufUnload(bufnr) } } public resolveRoot(rootPatterns: string[], requireRootPattern = false): string | undefined { let doc = this.getDocument(this.bufnr) let resolved: string | undefined if (doc && doc.schema == 'file') { let dir = path.dirname(URI.parse(doc.uri).fsPath) resolved = resolveRoot(dir, rootPatterns, this.cwd) } else { resolved = resolveRoot(this.cwd, rootPatterns) } if (requireRootPattern && !resolved) { throw new Error(`Required root pattern not resolved.`) } return resolved } public get textDocuments(): LinesTextDocument[] { let docs: LinesTextDocument[] = [] for (let b of this.buffers.values()) { if (b.attached) docs.push(b.textDocument) } return docs } public getDocument(uri: number | string, caseInsensitive = platform.isWindows || platform.isMacintosh): Document | null | undefined { if (typeof uri === 'number') { return this.buffers.get(uri) } let u = URI.parse(uri) uri = u.toString() let isFile = u.scheme === 'file' for (let doc of this.buffers.values()) { if (doc.uri === uri) return doc if (isFile && caseInsensitive && doc.uri.toLowerCase() === uri.toLowerCase()) return doc } return null } /** * Expand filepath with `~` and/or environment placeholders */ public expand(input: string): string { if (input.startsWith('~')) { input = os.homedir() + input.slice(1) } if (input.includes('$')) { let doc = this.getDocument(this.bufnr) let fsPath = doc ? URI.parse(doc.uri).fsPath : '' const root = this._root || this._cwd input = input.replace(/\$\{(.*?)\}/g, (match: string, name: string) => { if (name.startsWith('env:')) { let key = name.split(':')[1] let val = key ? process.env[key] : '' return val } switch (name) { case 'tmpdir': return os.tmpdir() case 'userHome': return os.homedir() case 'workspace': case 'workspaceRoot': case 'workspaceFolder': return root case 'workspaceFolderBasename': return path.basename(root) case 'cwd': return this._cwd case 'file': return fsPath case 'fileDirname': return fsPath ? path.dirname(fsPath) : '' case 'fileExtname': return fsPath ? path.extname(fsPath) : '' case 'fileBasename': return fsPath ? path.basename(fsPath) : '' case 'fileBasenameNoExtension': { let base = fsPath ? path.basename(fsPath) : '' return base ? base.slice(0, base.length - path.extname(base).length) : '' } default: return match } }) input = input.replace(/\$[\w]+/g, match => { if (match == '$HOME') return os.homedir() return process.env[match.slice(1)] || match }) } return input } /** * Current document. */ public get document(): Promise { if (this._currentResolve) { return new Promise(resolve => { this.resolves.push(resolve) }) } this._currentResolve = true return new Promise(resolve => { this.nvim.eval(`coc#util#get_bufoptions(bufnr("%"),${this.config.maxFileSize})`).then((opts: any) => { let doc: Document | undefined if (opts != null) { this.creating.delete(opts.bufnr) doc = this._createDocument(opts) } this.resolveCurrent(doc) resolve(doc) this._currentResolve = false }, () => { resolve(undefined) this._currentResolve = false }) }) } private resolveCurrent(document: Document | undefined): void { if (this.resolves.length > 0) { while (this.resolves.length) { const fn = this.resolves.pop() if (fn) fn(document) } } } public get uri(): string { let { bufnr } = this if (bufnr) { let doc = this.getDocument(bufnr) if (doc) return doc.uri } return null } /** * Current filetypes. */ public get filetypes(): Set { let res = new Set() for (let doc of this.attached()) { res.add(doc.filetype) } return res } /** * Get filetype by check same extension name buffer. */ public getLanguageId(filepath: string): string { let ext = path.extname(filepath) if (!ext) return '' for (let doc of this.attached()) { let fsPath = URI.parse(doc.uri).fsPath if (path.extname(fsPath) == ext) { return doc.languageId } } return '' } public async getLines(uri: string): Promise { let doc = this.getDocument(uri) if (doc) return doc.textDocument.lines let u = URI.parse(uri) if (u.scheme !== 'file') return [] try { let content = await readFile(u.fsPath, 'utf8') return content.split(/\r?\n/) } catch (e) { return [] } } /** * Current languageIds. */ public get languageIds(): Set { let res = new Set() for (let doc of this.attached()) { res.add(doc.languageId) } return res } /** * Get format options */ public async getFormatOptions(uri?: string | number): Promise { let bufnr = typeof uri === 'number' ? uri : this.getBufnr(uri) let res = await this.nvim.call('coc#util#get_format_opts', [bufnr]) as VimFormatOption return convertFormatOptions(res) } public getBufnr(uri?: string): number { if (!uri) return 0 let doc = this.getDocument(uri) return doc ? doc.bufnr : 0 } /** * Create document by bufnr. */ public async createDocument(bufnr: number): Promise { let doc = this.buffers.get(bufnr) if (doc) return doc if (this.creating.has(bufnr)) return await this.creating.get(bufnr) let promise = new Promise(resolve => { this.nvim.call('coc#util#get_bufoptions', [bufnr, this.config.maxFileSize]).then(opts => { if (!this.creating.has(bufnr)) { resolve(undefined) return } this.creating.delete(bufnr) if (!opts) { resolve(undefined) return } doc = this._createDocument(opts as BufferOption) resolve(doc) }, () => { this.creating.delete(bufnr) resolve(undefined) }) }) this.creating.set(bufnr, promise) return await promise } public async onBufCreate(bufnr: number): Promise { this.onBufUnload(bufnr) await this.createDocument(bufnr) } private _createDocument(opts: BufferOption): Document { let { bufnr } = opts if (this.buffers.has(bufnr)) return this.buffers.get(bufnr) let buffer = this.nvim.createBuffer(bufnr) let doc = new Document(buffer, this.nvim, this.convertFiletype(opts.filetype), opts) if (opts.size > this.config.maxFileSize) logger.warn(`buffer ${opts.bufnr} size exceed maxFileSize ${this.config.maxFileSize}, not attached.`) this.buffers.set(bufnr, doc) if (doc.attached) { if (doc.schema == 'file') { // TODO use workspaceFolder for root when exists this.configurations.locateFolderConfigution(doc.uri) let root = this.workspaceFolder.resolveRoot(doc, this._cwd, true, this.expand.bind(this)) if (root && bufnr == this._bufnr) this.changeRoot(root) } this._onDidOpenTextDocument.fire(doc.textDocument) doc.onDocumentChange(e => this._onDidChangeDocument.fire(e)) } logger.debug('buffer created', bufnr, doc.attached, doc.uri) return doc } private onBufEnter(bufnr: number): void { this._bufnr = bufnr let doc = this.buffers.get(bufnr) if (doc) { let workspaceFolder = this.workspaceFolder.getWorkspaceFolder(URI.parse(doc.uri)) if (workspaceFolder) this._root = URI.parse(workspaceFolder.uri).fsPath } } private onBufUnload(bufnr: number): void { this.creating.delete(bufnr) void this.onBufDetach(bufnr, false) } private async onBufDetach(bufnr: number, checkReload = true): Promise { this.clearTimer(bufnr) this.detachBuffer(bufnr) if (checkReload) { let loaded = await this.nvim.call('bufloaded', [bufnr]) if (loaded) await this.createDocument(bufnr) } } public detachBuffer(bufnr: number): void { let doc = this.buffers.get(bufnr) this.buffers.delete(bufnr) if (!doc || !doc.attached) return logger.debug('document detach', bufnr, doc.uri) this._onDidCloseDocument.fire(doc.textDocument) doc.detach() const uris = this.textDocuments.map(o => URI.parse(o.uri)) this.workspaceFolder.onDocumentDetach(uris) } private async onBufWritePost(bufnr: number, changedtick: number): Promise { let doc = this.buffers.get(bufnr) if (doc) { if (doc.changedtick != changedtick) await doc.patchChange() this._onDidSaveDocument.fire(doc.textDocument) } } private async onBufWritePre(bufnr: number, bufname: string, changedtick: number): Promise { let doc = this.buffers.get(bufnr) if (!doc || !doc.attached) return if (doc.bufname != bufname) { this.detachBuffer(bufnr) doc = await this.createDocument(bufnr) if (!doc.attached) return } if (doc.changedtick != changedtick) { await doc.synchronize() } else { await doc.patchChange() } let firing = true let thenables: Thenable[] = [] let event: TextDocumentWillSaveEvent = { bufnr: doc.bufnr, document: doc.textDocument, reason: TextDocumentSaveReason.Manual, waitUntil: (thenable: Thenable) => { if (!firing) { this.nvim.echoError(`waitUntil can't be used in async manner, check log for details`) } else { thenables.push(thenable) } } } this._onWillSaveDocument.fire(event) firing = false let total = thenables.length if (total) { let promise = new Promise(resolve => { const willSaveHandlerTimeout = this.config.willSaveHandlerTimeout let timer = setTimeout(() => { this.nvim.outWriteLine(`Will save handler timeout after ${willSaveHandlerTimeout}ms`) resolve(undefined) }, willSaveHandlerTimeout) let i = 0 let called = false for (let p of thenables) { let cb = (res: any) => { if (called) return called = true clearTimeout(timer) resolve(res) } p.then(res => { if (Array.isArray(res) && res.length && TextEdit.is(res[0])) { return cb(res) } i = i + 1 if (i == total) cb(undefined) }, e => { logger.error(`Error on will save handler:`, e) i = i + 1 if (i == total) cb(undefined) }) } }) let edits = await promise if (edits) await doc.applyEdits(edits, false, this.bufnr === doc.bufnr) } await this.tryCodeActionsOnSave(doc) await this.tryFormatOnSave(doc) } public async tryFormatOnSave(document: Document): Promise { if (!this.shouldFormatOnSave(document)) return let options = await this.getFormatOptions(document.uri) let formatOnSaveTimeout = this.config.formatOnSaveTimeout let timer: NodeJS.Timeout let tokenSource = new CancellationTokenSource() const tp = new Promise(c => { timer = setTimeout(() => { logger.warn(`Format on save timeout after ${formatOnSaveTimeout}ms`, document.uri) tokenSource.cancel() c(undefined) }, formatOnSaveTimeout) }) const provideEdits = languages.provideDocumentFormattingEdits(document.textDocument, options, tokenSource.token) let textEdits = await Promise.race([tp, provideEdits]) clearTimeout(timer) if (isFalsyOrEmpty(textEdits)) return await document.applyEdits(textEdits) let extensionName = textEdits['__extensionName'] logger.info(`Format buffer ${document.bufnr} by ${toText(extensionName)}`) } public async tryCodeActionsOnSave(doc: Document): Promise { let editorConfig = this.configurations.getConfiguration('editor', doc.textDocument) let conf = editorConfig.get('codeActionsOnSave', {}) if (emptyObject(conf)) return false const actions: string[] = [] for (const key of Object.keys(conf)) { if (conf[key] === true || conf[key] === 'always') { actions.push(key) } } if (actions.length === 0) return false await commands.executeCommand('editor.action.executeCodeActions', doc, undefined, actions, this.config.willSaveHandlerTimeout) return true } public shouldFormatOnSave(document: Document): boolean { if (!languages.hasFormatProvider(document)) { logger.warn(`Format provider not found for ${document.uri}`) return false } if (!document || document.getVar('disable_autoformat', 0)) { logger.warn(`Format ${document.uri} disabled by b:coc_disable_autoformat`) return false } let config = this.configurations.getConfiguration('coc.preferences', document) let filetypes = config.get('formatOnSaveFiletypes', null) if (Array.isArray(filetypes)) return filetypes.includes('*') || filetypes.includes(document.languageId) let formatOnSave = config.get('formatOnSave', false) return formatOnSave } public onFileTypeChange(filetype: string, bufnr: number): void { let doc = this.getDocument(bufnr) if (!doc) return this.clearTimer(bufnr) let timer = setTimeout(() => { if (this.creating.has(bufnr) || !doc.attached) return let converted = this.convertFiletype(filetype) if (converted === doc.filetype) return this._onDidCloseDocument.fire(doc.textDocument) doc.setFiletype(filetype) this._onDidOpenTextDocument.fire(doc.textDocument) }, filetypeDelay) this._filetypeTimer.set(bufnr, timer) } public async getQuickfixList(locations: LocationWithTarget[]): Promise> { let filesLines: { [fsPath: string]: string[] } = {} let filepathList = locations.reduce((pre: string[], curr) => { let u = URI.parse(curr.uri) if (u.scheme == 'file' && !pre.includes(u.fsPath) && !this.getDocument(curr.uri)) { pre.push(u.fsPath) } return pre }, []) await Promise.all(filepathList.map(fsPath => { return new Promise(resolve => { readFile(fsPath, 'utf8').then(content => { filesLines[fsPath] = content.split(/\r?\n/) resolve(undefined) }, () => { resolve() }) }) })) return await Promise.all(locations.map(loc => { let { uri, range } = loc let { fsPath } = URI.parse(uri) let text: string | undefined let lines = filesLines[fsPath] if (lines) text = lines[range.start.line] return this.getQuickfixItem(loc, text) })) } /** * Populate locations to UI. */ public async showLocations(locations: LocationWithTarget[]): Promise { let { nvim } = this let items = await this.getQuickfixList(locations) if (this.config.useQuickfixForLocations) { let openCommand = await nvim.getVar('coc_quickfix_open_command') as string if (typeof openCommand != 'string') { openCommand = items.length < 10 ? `copen ${items.length}` : 'copen' } nvim.pauseNotification() nvim.call('setqflist', [items], true) nvim.command(openCommand, true) nvim.resumeNotification(false, true) } else { await nvim.setVar('coc_jump_locations', items) if (this._env.locationlist) { nvim.command('CocList --normal --auto-preview location', true) } else { nvim.call('coc#util#do_autocmd', ['CocLocationsChange'], true) } } } public fixUnixPrefix(filepath: string): string { if (!this._env.isCygwin || !/^\w:/.test(filepath)) return filepath return this._env.unixPrefix + filepath[0].toLowerCase() + filepath.slice(2).replace(/\\/g, '/') } /** * Convert location to quickfix item. */ public async getQuickfixItem(loc: LocationWithTarget | LocationLink, text?: string, type = '', module?: string): Promise { let targetRange = loc.targetRange if (LocationLink.is(loc)) { loc = Location.create(loc.targetUri, loc.targetRange) } let doc = this.getDocument(loc.uri) let { uri, range } = loc let { start, end } = range let u = URI.parse(uri) if (!text && u.scheme == 'file') { text = await this.getLine(uri, start.line) } let endLine = start.line == end.line ? text : await this.getLine(uri, end.line) let item: QuickfixItem = { uri, filename: u.scheme == 'file' ? this.fixUnixPrefix(u.fsPath) : uri, lnum: start.line + 1, end_lnum: end.line + 1, col: text ? byteIndex(text, start.character) + 1 : start.character + 1, end_col: endLine ? byteIndex(endLine, end.character) + 1 : end.character + 1, text: text || '', range } if (targetRange) item.targetRange = targetRange if (module) item.module = module if (type) item.type = type if (doc) item.bufnr = doc.bufnr return item } /** * Get content of line by uri and line. */ public async getLine(uri: string, line: number): Promise { let document = this.getDocument(uri) if (document && document.attached) return document.getline(line) || '' if (!uri.startsWith('file:')) return '' let fsPath = URI.parse(uri).fsPath if (!fs.existsSync(fsPath)) return '' return await readFileLine(fsPath, line) } /** * Get content from buffer or file by uri. */ public async readFile(uri: string): Promise { let document = this.getDocument(uri) if (document) { await document.patchChange() return document.content } let u = URI.parse(uri) if (u.scheme != 'file') return '' let lines = await this.nvim.call('readfile', [u.fsPath]) as string[] return lines.join('\n') + '\n' } private clearTimer(bufnr: number): void { let timer = this._filetypeTimer.get(bufnr) if (timer) clearTimeout(timer) } public convertFiletype(filetype: string): string { switch (filetype) { case 'javascript.jsx': return 'javascriptreact' case 'typescript.jsx': case 'typescript.tsx': return 'typescriptreact' case 'tex': // Vim filetype 'tex' means LaTeX, which has LSP language ID 'latex' return 'latex' default: { let map = toObject(this._env.filetypeMap) return toText(hasOwnProperty(map, filetype) ? map[filetype] : filetype) } } } public reset(): void { this.creating.clear() for (let bufnr of this.buffers.keys()) { this.onBufUnload(bufnr) } this.buffers.clear() this.changeRoot(process.cwd()) } private changeRoot(dir: string): void { this._root = normalizeFilePath(dir) } public dispose(): void { for (let bufnr of this.buffers.keys()) { this.onBufUnload(bufnr) } this._attached = false this.buffers.clear() disposeAll(this.disposables) } } ================================================ FILE: src/core/editors.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { FormattingOptions, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import events from '../events' import { createLogger } from '../logger' import type Document from '../model/document' import { convertFormatOptions, VimFormatOption } from '../util/convert' import { onUnexpectedError } from '../util/errors' import { sameFile } from '../util/fs' import { Mutex } from '../util/mutex' import { Disposable, Emitter, Event } from '../util/protocol' import Documents from './documents' const logger = createLogger('core-editors') interface EditorOption { bufnr: number winid: number tabpageid: number winnr: number visibleRanges: [number, number][] tabSize: number insertSpaces: boolean formatOptions: VimFormatOption } interface EditorInfo { readonly winid: number readonly bufnr: number readonly tabid: number readonly fullpath: string } export interface TextEditor { readonly id: string readonly tabpageid: number readonly winid: number readonly winnr: number readonly document: Document readonly visibleRanges: readonly Range[] readonly uri: string readonly bufnr: number readonly options: FormattingOptions } export function renamed(editor: TextEditor, info: EditorInfo): boolean { let { document, uri } = editor if (document.bufnr != info.bufnr) return false let u = URI.parse(uri) if (u.scheme === 'file') return !sameFile(u.fsPath, info.fullpath) return false } export default class Editors { private disposables: Disposable[] = [] private winid = -1 private mutex: Mutex = new Mutex() private previousId: string | undefined private nvim: Neovim private editors: Map = new Map() private tabIds: Set = new Set() private creating: Set = new Set() private readonly _onDidTabClose = new Emitter() private readonly _onDidChangeActiveTextEditor = new Emitter() private readonly _onDidChangeVisibleTextEditors = new Emitter>() public readonly onDidTabClose: Event = this._onDidTabClose.event public readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event public readonly onDidChangeVisibleTextEditors: Event> = this._onDidChangeVisibleTextEditors.event constructor(private documents: Documents) { } public get activeTextEditor(): TextEditor | undefined { return this.editors.get(this.winid) } public get visibleTextEditors(): TextEditor[] { return Array.from(this.editors.values()) } public getFormatOptions(bufnr: number | string): FormattingOptions | undefined { for (let editor of this.editors.values()) { if (editor.bufnr === bufnr || editor.uri === bufnr) return editor.options } return undefined } public getBufWinids(bufnr: number): number[] { let winids: number[] = [] for (let editor of this.editors.values()) { if (editor.bufnr == bufnr) winids.push(editor.winid) } return winids } private onChangeCurrent(editor: TextEditor | undefined): void { if (!editor) return let id = editor.id if (id === this.previousId) return this.previousId = id this._onDidChangeActiveTextEditor.fire(editor) } public async attach(nvim: Neovim): Promise { this.nvim = nvim let [winid, infos] = await nvim.eval(`[win_getid(),coc#util#editor_infos()]`) as [number, EditorInfo[]] await Promise.allSettled(infos.map(info => { return this.createTextEditor(info.winid) })) this.winid = winid events.on('CursorHold', this.checkEditors, this, this.disposables) events.on('TabNew', (tabid: number) => { this.tabIds.add(tabid) }, null, this.disposables) events.on('TabClosed', this.checkTabs, this, this.disposables) events.on('WinEnter', (winid: number) => { this.winid = winid let editor = this.editors.get(winid) if (editor) this.onChangeCurrent(editor) }, null, this.disposables) events.on('WinClosed', (winid: number) => { if (this.editors.has(winid)) { this.editors.delete(winid) this._onDidChangeVisibleTextEditors.fire(this.visibleTextEditors) } }, null, this.disposables) events.on('BufWinEnter', async (_: number, winid: number) => { this.winid = winid let changed = await this.createTextEditor(winid) if (changed) this._onDidChangeVisibleTextEditors.fire(this.visibleTextEditors) }, null, this.disposables) this.documents.onDidOpenTextDocument(async e => { let document = this.documents.getDocument(e.bufnr) let changed = false for (let winid of document.winids) { let editor = this.editors.get(winid) // buffer can be reloaded if (editor?.document !== document) { let res = await this.createTextEditor(winid).catch(onUnexpectedError) if (res) changed = true } } if (changed) this._onDidChangeVisibleTextEditors.fire(this.visibleTextEditors) }, null, this.disposables) } public checkTabs(ids: number[]): void { let changed = false for (let editor of this.editors.values()) { if (!ids.includes(editor.tabpageid)) { changed = true this.editors.delete(editor.winid) } } for (let id of Array.from(this.tabIds)) { if (!ids.includes(id)) this._onDidTabClose.fire(id) } this.tabIds = new Set(ids) if (changed) this._onDidChangeVisibleTextEditors.fire(this.visibleTextEditors) } public checkUnloadedBuffers(bufnrs: number[]): void { for (let bufnr of this.documents.bufnrs) { if (!bufnrs.includes(bufnr)) { void events.fire('BufUnload', [bufnr]) } } } public async checkEditors(): Promise { let { documents } = this await this.mutex.use(async () => { let [winid, bufnrs, infos] = await this.nvim.eval(`[win_getid(),coc#util#get_loaded_bufs(),coc#util#editor_infos()]`) as [number, number[], EditorInfo[]] this.winid = winid this.checkUnloadedBuffers(bufnrs) let changed = false let winids: Set = new Set() for (let info of infos) { let editor = this.editors.get(info.winid) let create = false if (!editor) { create = true } else if (renamed(editor, info)) { void events.fire('BufRename', [info.bufnr]) create = true } else if (editor.document.bufnr != info.bufnr || editor.document !== documents.getDocument(info.bufnr) || editor.tabpageid != info.tabid) { create = true } if (create) { await this.createTextEditor(info.winid) changed = true } winids.add(info.winid) } if (this.cleanUpEditors(winids)) { changed = true } this.onChangeCurrent(this.activeTextEditor) if (changed) this._onDidChangeVisibleTextEditors.fire(this.visibleTextEditors) }) } public cleanUpEditors(winids: Set): boolean { let changed = false for (let winid of Array.from(this.editors.keys())) { if (!winids.has(winid)) { changed = true this.editors.delete(winid) } } return changed } private async createTextEditor(winid: number): Promise { let { documents, creating, nvim } = this if (creating.has(winid)) return false let changed = false creating.add(winid) let opts = await nvim.call('coc#util#get_editoroption', [winid]) as EditorOption if (opts) { this.tabIds.add(opts.tabpageid) let doc = documents.getDocument(opts.bufnr) if (doc && doc.attached) { let editor = this.fromOptions(opts) this.editors.set(winid, editor) if (winid == this.winid) this.onChangeCurrent(editor) logger.debug('editor created winid & bufnr & tabpageid: ', winid, opts.bufnr, opts.tabpageid) changed = true } else if (this.editors.has(winid)) { this.editors.delete(winid) changed = true } } creating.delete(winid) return changed } private fromOptions(opts: EditorOption): TextEditor { let { visibleRanges, bufnr, formatOptions } = opts let { documents } = this let document = documents.getDocument(bufnr) return { id: `${opts.tabpageid}-${opts.winid}-${document.uri}`, tabpageid: opts.tabpageid, winid: opts.winid, winnr: opts.winnr, uri: document.uri, bufnr: document.bufnr, document, visibleRanges: visibleRanges.map(o => Range.create(o[0] - 1, 0, o[1], 0)), options: convertFormatOptions(formatOptions) } } } ================================================ FILE: src/core/fileSystemWatcher.ts ================================================ 'use strict' import { WorkspaceFolder } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { createLogger } from '../logger' import { FileWatchConfig, GlobPattern, IFileSystemWatcher, OutputChannel } from '../types' import { disposeAll } from '../util' import { splitArray } from '../util/array' import { isFolderIgnored, isParentFolder, sameFile } from '../util/fs' import { minimatch, path, which } from '../util/node' import { Disposable, Emitter, Event } from '../util/protocol' import Watchman, { FileChange } from './watchman' import type WorkspaceFolderControl from './workspaceFolder' const logger = createLogger('fileSystemWatcher') const WATCHMAN_COMMAND = 'watchman' export interface RenameEvent { oldUri: URI newUri: URI } export class FileSystemWatcherManager { private clientsMap: Map = new Map() private disposables: Disposable[] = [] private channel: OutputChannel | undefined private creating: Set = new Set() public static watchers: Set = new Set() private readonly _onDidCreateClient = new Emitter() public disabled = global.__TEST__ public readonly onDidCreateClient: Event = this._onDidCreateClient.event constructor( private workspaceFolder: WorkspaceFolderControl, private config: FileWatchConfig ) { this.disabled = config.enable === false } public attach(channel: OutputChannel): void { this.channel = channel let createClient = (folder: WorkspaceFolder) => { let root = URI.parse(folder.uri).fsPath void this.createClient(root) } this.workspaceFolder.workspaceFolders.forEach(folder => { createClient(folder) }) this.workspaceFolder.onDidChangeWorkspaceFolders(e => { e.added.forEach(folder => { createClient(folder) }) e.removed.forEach(folder => { let root = URI.parse(folder.uri).fsPath let client = this.clientsMap.get(root) if (client) { this.clientsMap.delete(root) client.dispose() } }) }, null, this.disposables) } public waitClient(root: string): Promise { if (this.clientsMap.has(root)) return Promise.resolve(this.clientsMap.get(root)) return new Promise(resolve => { let disposable = this.onDidCreateClient(r => { if (r == root) { disposable.dispose() resolve(this.clientsMap.get(r)) } }) }) } public async createClient(root: string, skipCheck = false): Promise { if (!skipCheck && (this.disabled || isFolderIgnored(root, this.config.ignoredFolders))) return if (this.has(root)) return this.waitClient(root) try { this.creating.add(root) let watchmanPath = await this.getWatchmanPath() let client = await Watchman.createClient(watchmanPath, root, this.channel) this.creating.delete(root) this.clientsMap.set(root, client) for (let watcher of FileSystemWatcherManager.watchers) { watcher.listen(root, client) } this._onDidCreateClient.fire(root) return client } catch (e) { this.creating.delete(root) if (this.channel) this.channel.appendLine(`Error on create watchman client: ${e}`) return false } } public async getWatchmanPath(): Promise { let watchmanPath = this.config.watchmanPath ?? WATCHMAN_COMMAND if (!process.env.WATCHMAN_SOCK) { watchmanPath = await which(watchmanPath, { all: false }) } return watchmanPath } private has(root: string): boolean { let curr = Array.from(this.clientsMap.keys()) curr.push(...this.creating) return curr.some(r => sameFile(r, root)) } public createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents: boolean, ignoreChangeEvents: boolean, ignoreDeleteEvents: boolean): FileSystemWatcher { let fileWatcher = new FileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents) let base = typeof globPattern === 'string' ? undefined : globPattern.baseUri.fsPath for (let [root, client] of this.clientsMap.entries()) { if (base && isParentFolder(root, base, true)) { base = undefined } fileWatcher.listen(root, client) } if (base) void this.createClient(base) FileSystemWatcherManager.watchers.add(fileWatcher) return fileWatcher } public dispose(): void { this._onDidCreateClient.dispose() for (let client of this.clientsMap.values()) { if (client) client.dispose() } this.clientsMap.clear() FileSystemWatcherManager.watchers.clear() disposeAll(this.disposables) } } /* * FileSystemWatcher for watch workspace folders. */ export class FileSystemWatcher implements IFileSystemWatcher { private _onDidCreate = new Emitter() private _onDidChange = new Emitter() private _onDidDelete = new Emitter() private _onDidRename = new Emitter() private disposables: Disposable[] = [] public subscribe: string public readonly onDidCreate: Event = this._onDidCreate.event public readonly onDidChange: Event = this._onDidChange.event public readonly onDidDelete: Event = this._onDidDelete.event public readonly onDidRename: Event = this._onDidRename.event private readonly _onDidListen = new Emitter() public readonly onDidListen: Event = this._onDidListen.event constructor( private globPattern: GlobPattern, public ignoreCreateEvents: boolean, public ignoreChangeEvents: boolean, public ignoreDeleteEvents: boolean, ) { } public listen(root: string, client: Watchman): void { let { globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents } = this let pattern: string let basePath: string | undefined if (typeof globPattern === 'string') { pattern = globPattern } else { pattern = globPattern.pattern basePath = globPattern.baseUri.fsPath // ignore client if (!isParentFolder(root, basePath, true)) return } const onChange = (change: FileChange) => { let { root, files } = change if (basePath && !sameFile(root, basePath)) { files = files.filter(f => { if (f.type != 'f') return false let fullpath = path.join(root, f.name) if (!isParentFolder(basePath, fullpath)) return false return minimatch(path.relative(basePath, fullpath), pattern, { dot: true }) }) } else { files = files.filter(f => f.type == 'f' && minimatch(f.name, pattern, { dot: true })) } for (let file of files) { let uri = URI.file(path.join(root, file.name)) if (!file.exists) { if (!ignoreDeleteEvents) this._onDidDelete.fire(uri) } else { if (file.new === true) { if (!ignoreCreateEvents) this._onDidCreate.fire(uri) } else { if (!ignoreChangeEvents) this._onDidChange.fire(uri) } } } // file rename if (files.length == 2 && files[0].exists !== files[1].exists) { let oldFile = files.find(o => o.exists !== true) let newFile = files.find(o => o.exists === true) if (oldFile.size == newFile.size) { this._onDidRename.fire({ oldUri: URI.file(path.join(root, oldFile.name)), newUri: URI.file(path.join(root, newFile.name)) }) } } // detect folder rename if (files.length > 2 && files.length % 2 == 0) { let [oldFiles, newFiles] = splitArray(files, o => o.exists === false) if (oldFiles.length == newFiles.length) { for (let oldFile of oldFiles) { let newFile = newFiles.find(o => o.size == oldFile.size && o.mtime_ms == oldFile.mtime_ms) if (newFile) { this._onDidRename.fire({ oldUri: URI.file(path.join(root, oldFile.name)), newUri: URI.file(path.join(root, newFile.name)) }) } } } } } this.subscribe = client.subscription let disposable = client.subscribe(pattern, onChange) this._onDidListen.fire() this.disposables.push(disposable) } public dispose(): void { FileSystemWatcherManager.watchers.delete(this) this._onDidRename.dispose() this._onDidCreate.dispose() this._onDidChange.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/core/files.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import type { TextDocument } from 'vscode-languageserver-textdocument' import { ChangeAnnotation, CreateFile, CreateFileOptions, DeleteFile, DeleteFileOptions, Position, RenameFile, RenameFileOptions, SnippetTextEdit, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import Configurations from '../configuration' import events from '../events' import { createLogger } from '../logger' import Document from '../model/document' import EditInspect, { EditState, RecoverFunc } from '../model/editInspect' import type { SnippetEdit } from '../snippets/session' import { DocumentChange, Env, GlobPattern } from '../types' import * as errors from '../util/errors' import { isFile, isParentFolder, normalizeFilePath, statAsync } from '../util/fs' import { crypto, fs, glob, minimatch, os, path } from '../util/node' import { CancellationToken, CancellationTokenSource, Emitter, Event, TextDocumentSaveReason } from '../util/protocol' import { byteIndex } from '../util/string' import { createFilteredChanges, getConfirmAnnotations, getRevertEdit, mergeSortEdits, toDocumentChanges } from '../util/textedit' import type { Window } from '../window' import Documents from './documents' import type Keymaps from './keymaps' import WorkspaceFolderController from './workspaceFolder' const logger = createLogger('core-files') export interface LinesChange { uri: string lnum: number oldLines: ReadonlyArray newLines: ReadonlyArray } /** * An event that is fired when a [document](#TextDocument) will be saved. * * To make modifications to the document before it is being saved, call the * [`waitUntil`](#TextDocumentWillSaveEvent.waitUntil)-function with a thenable * that resolves to an array of [text edits](#TextEdit). */ export interface TextDocumentWillSaveEvent { bufnr: number /** * The document that will be saved. */ document: TextDocument /** * The reason why save was triggered. */ reason: TextDocumentSaveReason /** * Allows to pause the event loop and to apply [pre-save-edits](#TextEdit). * Edits of subsequent calls to this function will be applied in order. The * edits will be *ignored* if concurrent modifications of the document happened. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * @param thenable A thenable that resolves to [pre-save-edits](#TextEdit). */ waitUntil(thenable: Thenable): void } /** * An event that is fired when files are going to be renamed. * * To make modifications to the workspace before the files are renamed, * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a * thenable that resolves to a [workspace edit](#WorkspaceEdit). */ export interface FileWillRenameEvent { /** * The files that are going to be renamed. */ readonly files: ReadonlyArray<{ oldUri: URI, newUri: URI }> /** * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillCreateFiles(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * @param thenable A thenable that delays saving. */ waitUntil(thenable: Thenable): void } /** * An event that is fired after files are renamed. */ export interface FileRenameEvent { /** * The files that got renamed. */ readonly files: ReadonlyArray<{ oldUri: URI, newUri: URI }> } /** * An event that is fired when files are going to be created. * * To make modifications to the workspace before the files are created, * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a * thenable that resolves to a [workspace edit](#WorkspaceEdit). */ export interface FileWillCreateEvent { /** * A cancellation token. */ readonly token: CancellationToken /** * The files that are going to be created. */ readonly files: ReadonlyArray /** * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillCreateFiles(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * @param thenable A thenable that delays saving. */ waitUntil(thenable: Thenable): void } /** * An event that is fired after files are created. */ export interface FileCreateEvent { /** * The files that got created. */ readonly files: ReadonlyArray } /** * An event that is fired when files are going to be deleted. * * To make modifications to the workspace before the files are deleted, * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a * thenable that resolves to a [workspace edit](#WorkspaceEdit). */ export interface FileWillDeleteEvent { /** * The files that are going to be deleted. */ readonly files: ReadonlyArray /** * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillCreateFiles(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * @param thenable A thenable that delays saving. */ waitUntil(thenable: Thenable): void } /** * An event that is fired after files are deleted. */ export interface FileDeleteEvent { /** * The files that got deleted. */ readonly files: ReadonlyArray } interface WaitUntilEvent { waitUntil(thenable: Thenable): void } export default class Files { private nvim: Neovim private env: Env private window: Window private editState: EditState | undefined private operationTimeout = 500 private _onDidCreateFiles = new Emitter() private _onDidRenameFiles = new Emitter() private _onDidDeleteFiles = new Emitter() private _onWillCreateFiles = new Emitter() private _onWillRenameFiles = new Emitter() private _onWillDeleteFiles = new Emitter() public readonly onDidCreateFiles: Event = this._onDidCreateFiles.event public readonly onDidRenameFiles: Event = this._onDidRenameFiles.event public readonly onDidDeleteFiles: Event = this._onDidDeleteFiles.event public readonly onWillCreateFiles: Event = this._onWillCreateFiles.event public readonly onWillRenameFiles: Event = this._onWillRenameFiles.event public readonly onWillDeleteFiles: Event = this._onWillDeleteFiles.event constructor( private documents: Documents, private configurations: Configurations, private workspaceFolderControl: WorkspaceFolderController, private keymaps: Keymaps ) { } public attach(nvim: Neovim, env: Env, window: Window): void { this.nvim = nvim this.env = env this.window = window } public async openTextDocument(uri: URI | string): Promise { uri = typeof uri === 'string' ? URI.file(uri) : uri let doc = this.documents.getDocument(uri.toString()) if (doc) return doc const scheme = uri.scheme if (scheme == 'file') { if (!fs.existsSync(uri.fsPath)) throw errors.fileNotExists(uri.fsPath) fs.accessSync(uri.fsPath, fs.constants.R_OK) } if (scheme == 'untitled') { await this.nvim.call('coc#util#open_file', ['tab drop', uri.path]) return await this.documents.document } return await this.loadResource(uri.toString(), null) } public async jumpTo(uri: string | URI, position?: Position | null, openCommand?: string): Promise { if (!openCommand) openCommand = this.configurations.initialConfiguration.get('coc.preferences.jumpCommand', 'edit') let { nvim } = this let u = uri instanceof URI ? uri : URI.parse(uri) let doc = this.documents.getDocument(u.with({ fragment: '' }).toString()) let bufnr = doc ? doc.bufnr : -1 if (!position && u.scheme === 'file' && u.fragment) { let parts = u.fragment.split(',') let lnum = parseInt(parts[0], 10) if (!isNaN(lnum)) { let col = parts.length > 0 && /^\d+$/.test(parts[1]) ? parseInt(parts[1], 10) : undefined position = Position.create(lnum - 1, col == null ? 0 : col - 1) } } if (bufnr != -1 && openCommand == 'edit') { // use buffer command since edit command would reload the buffer nvim.pauseNotification() nvim.command(`silent! normal! m'`, true) nvim.command(`buffer ${bufnr}`, true) nvim.command(`if &filetype ==# '' | filetype detect | endif`, true) if (position) { let line = doc.getline(position.line) let col = byteIndex(line, position.character) + 1 nvim.call('cursor', [position.line + 1, col], true) } await nvim.resumeNotification(true) } else { let { fsPath, scheme } = u let pos = position == null ? null : [position.line, position.character] if (scheme == 'file') { let bufname = normalizeFilePath(fsPath) await this.nvim.call('coc#util#jump', [openCommand, bufname, pos]) } else { await this.nvim.call('coc#util#jump', [openCommand, uri.toString(), pos]) } } } /** * Open resource by uri */ public async openResource(uri: string): Promise { let { nvim } = this let u = URI.parse(uri) if (/^https?/.test(u.scheme)) { await nvim.call('coc#ui#open_url', uri) return } await this.jumpTo(uri) await this.documents.document } /** * Load uri as document. */ public async loadResource(uri: string, cmd?: string): Promise { let doc = this.documents.getDocument(uri) if (doc) return doc if (cmd === undefined) { const preferences = this.configurations.getConfiguration('workspace') cmd = preferences.get('openResourceCommand', 'tab drop') } let u = URI.parse(uri) let bufname = u.scheme === 'file' ? u.fsPath : uri let bufnr: number if (cmd) { let winid = await this.nvim.call('win_getid') as number bufnr = await this.nvim.call('coc#util#open_file', [cmd, bufname]) as number await this.nvim.call('win_gotoid', [winid]) } else { let arr = await this.nvim.call('coc#ui#open_files', [[bufname]]) bufnr = arr[0] } return await this.documents.createDocument(bufnr) } /** * Load the files that not loaded */ public async loadResources(uris: string[]): Promise<(Document | undefined)[]> { let { documents } = this let files = uris.map(uri => { let u = URI.parse(uri) return u.scheme == 'file' ? u.fsPath : uri }) let bufnrs = await this.nvim.call('coc#ui#open_files', [files]) as number[] return await Promise.all(bufnrs.map(bufnr => { return documents.createDocument(bufnr) })) } /** * Create a file in vim and disk */ public async createFile(filepath: string, opts: CreateFileOptions = {}, recovers?: RecoverFunc[]): Promise { let { nvim } = this let exists = fs.existsSync(filepath) if (exists && !opts.overwrite && !opts.ignoreIfExists) { throw errors.fileExists(filepath) } if (!exists || opts.overwrite) { let tokenSource = new CancellationTokenSource() await this.fireWaitUntilEvent(this._onWillCreateFiles, { files: [URI.file(filepath)], token: tokenSource.token }, recovers) tokenSource.cancel() let dir = path.dirname(filepath) if (!fs.existsSync(dir)) { let folder: string let curr = dir while (!['.', '/', path.parse(dir).root].includes(curr)) { if (fs.existsSync(path.dirname(curr))) { folder = curr break } curr = path.dirname(curr) } fs.mkdirSync(dir, { recursive: true }) if (Array.isArray(recovers)) { recovers.push(() => { fs.rmSync(folder, { force: true, recursive: true }) }) } } fs.writeFileSync(filepath, '', 'utf8') if (Array.isArray(recovers)) { recovers.push(() => { fs.rmSync(filepath, { force: true, recursive: true }) }) } let doc = await this.loadResource(filepath) let bufnr = doc.bufnr if (Array.isArray(recovers)) { recovers.push(() => { void events.fire('BufUnload', [bufnr]) return nvim.command(`silent! bd! ${bufnr}`) }) } this._onDidCreateFiles.fire({ files: [URI.file(filepath)] }) } } /** * Delete a file or folder from vim and disk. */ public async deleteFile(filepath: string, opts: DeleteFileOptions = {}, recovers?: RecoverFunc[]): Promise { let { ignoreIfNotExists, recursive } = opts let stat = await statAsync(filepath) let isDir = stat && stat.isDirectory() if (!stat && !ignoreIfNotExists) { throw errors.fileNotExists(filepath) } if (stat == null) return let uri = URI.file(filepath) await this.fireWaitUntilEvent(this._onWillDeleteFiles, { files: [uri] }, recovers) if (!isDir) { let bufnr = await this.nvim.call('bufnr', [filepath]) if (bufnr) { void events.fire('BufUnload', [bufnr]) await this.nvim.command(`silent! bwipeout ${bufnr}`) if (Array.isArray(recovers)) { recovers.push(() => { return this.loadResource(uri.toString()) }) } } } let folder = path.join(os.tmpdir(), 'coc-' + process.pid) fs.mkdirSync(folder, { recursive: true }) let md5 = crypto.createHash('md5').update(filepath).digest('hex') if (isDir && recursive) { let dest = path.join(folder, md5) let dir = path.dirname(filepath) fs.renameSync(filepath, dest) if (Array.isArray(recovers)) { recovers.push(async () => { fs.mkdirSync(dir, { recursive: true }) fs.renameSync(dest, filepath) }) } } else if (isDir) { fs.rmdirSync(filepath) if (Array.isArray(recovers)) { recovers.push(() => { fs.mkdirSync(filepath) }) } } else { let dest = path.join(folder, md5) let dir = path.dirname(filepath) fs.renameSync(filepath, dest) if (Array.isArray(recovers)) { recovers.push(() => { fs.mkdirSync(dir, { recursive: true }) fs.renameSync(dest, filepath) }) } } this._onDidDeleteFiles.fire({ files: [uri] }) } /** * Rename a file or folder on vim and disk */ public async renameFile(oldPath: string, newPath: string, opts: RenameFileOptions & { skipEvent?: boolean } = {}, recovers?: RecoverFunc[]): Promise { let { nvim } = this let { overwrite, ignoreIfExists } = opts if (newPath === oldPath) return let exists = fs.existsSync(newPath) if (exists && ignoreIfExists && !overwrite) return if (exists && !overwrite) throw errors.fileExists(newPath) let oldStat = await statAsync(oldPath) let loaded = (oldStat && oldStat.isDirectory()) ? 0 : await nvim.call('bufloaded', [oldPath]) if (!loaded && !oldStat) throw errors.fileNotExists(oldPath) let file = { newUri: URI.parse(newPath), oldUri: URI.parse(oldPath) } if (!opts.skipEvent) await this.fireWaitUntilEvent(this._onWillRenameFiles, { files: [file] }, recovers) if (loaded) { let bufnr = await nvim.call('coc#ui#rename_file', [oldPath, newPath, oldStat != null]) as number await this.documents.onBufCreate(bufnr) } else { if (oldStat.isDirectory()) { for (let doc of this.documents.attached('file')) { let u = URI.parse(doc.uri) if (isParentFolder(oldPath, u.fsPath, false)) { let filepath = u.fsPath.replace(oldPath, newPath) let bufnr = await nvim.call('coc#ui#rename_file', [u.fsPath, filepath, false]) as number await this.documents.onBufCreate(bufnr) } } } fs.renameSync(oldPath, newPath) } if (Array.isArray(recovers)) { recovers.push(() => { return this.renameFile(newPath, oldPath, { skipEvent: true }) }) } if (!opts.skipEvent) this._onDidRenameFiles.fire({ files: [file] }) } /** * Return denied annotations */ private async promptAnnotations(documentChanges: DocumentChange[], changeAnnotations: { [id: string]: ChangeAnnotation } | undefined): Promise { let toConfirm = changeAnnotations ? getConfirmAnnotations(documentChanges, changeAnnotations) : [] let denied: string[] = [] for (let key of toConfirm) { let annotation = changeAnnotations[key] let res = await this.window.showMenuPicker(['Yes', 'No'], { position: 'center', title: 'Confirm edits', content: annotation.label + (annotation.description ? ' ' + annotation.description : '') }) if (res !== 0) denied.push(key) } return denied } /** * Apply WorkspaceEdit. */ public async applyEdit(edit: WorkspaceEdit, nested?: boolean): Promise { let documentChanges = toDocumentChanges(edit) let recovers: RecoverFunc[] = [] let currentOnly = false try { let denied = await this.promptAnnotations(documentChanges, edit.changeAnnotations) if (denied.length > 0) documentChanges = createFilteredChanges(documentChanges, denied) let changes: { [uri: string]: LinesChange } = {} let currentUri = await this.documents.getCurrentUri() currentOnly = documentChanges.every(o => TextDocumentEdit.is(o) && o.textDocument.uri === currentUri) this.validateChanges(documentChanges) for (const change of documentChanges) { if (TextDocumentEdit.is(change)) { let { textDocument, edits } = change let { uri } = textDocument let doc = await this.loadResource(uri) let revertEdit: TextEdit | undefined if (edits.some(o => SnippetTextEdit.is(o))) { // convert all to SnippetEdit let snippetEdits: SnippetEdit[] = mergeSortEdits(edits.map(edit => { if (SnippetTextEdit.is(edit)) { return { range: edit.range, snippet: edit.snippet.value } } return { range: edit.range, snippet: edit.newText } })) let oldLines = doc.textDocument.lines await commands.executeCommand('editor.action.insertBufferSnippets', doc.bufnr, snippetEdits, doc.bufnr === events.bufnr) let startLine = snippetEdits[0].range.start.line revertEdit = getRevertEdit(oldLines, doc.textDocument.lines, startLine) } else { revertEdit = await doc.applyEdits(edits as TextEdit[], false, uri === currentUri) } if (revertEdit) { let version = doc.version let { newText, range } = revertEdit changes[uri] = { uri, lnum: range.start.line + 1, newLines: doc.getLines(range.start.line, range.end.line), oldLines: newText.endsWith('\n') ? newText.slice(0, -1).split('\n') : newText.split('\n') } recovers.push(async () => { let doc = this.documents.getDocument(uri) if (!doc || !doc.attached || doc.version !== version) return await doc.applyEdits([revertEdit]) textDocument.version = doc.version }) } } else if (CreateFile.is(change)) { await this.createFile(fsPath(change.uri), change.options, recovers) } else if (DeleteFile.is(change)) { await this.deleteFile(fsPath(change.uri), change.options, recovers) } else if (RenameFile.is(change)) { await this.renameFile(fsPath(change.oldUri), fsPath(change.newUri), change.options, recovers) } } // nothing changed if (recovers.length === 0) return true if (!nested) this.editState = { edit: { documentChanges, changeAnnotations: edit.changeAnnotations }, changes, recovers, applied: true } this.nvim.redrawVim() } catch (e) { logger.error('Error on applyEdits:', edit, e) if (!nested) void this.window.showErrorMessage(`Error on applyEdits: ${e}`) await this.undoChanges(recovers) return false } // avoid message when change current file only. if (nested || currentOnly) return true void this.window.showInformationMessage(`Use ':wa' to save changes or ':CocCommand workspace.inspectEdit' to inspect.`) return true } private async undoChanges(recovers: RecoverFunc[]): Promise { while (recovers.length > 0) { let fn = recovers.pop() await Promise.resolve(fn()) } } public async inspectEdit(): Promise { if (!this.editState) { void this.window.showWarningMessage('No workspace edit to inspect') return } let inspect = new EditInspect(this.nvim, this.keymaps) await inspect.show(this.editState) } public async undoWorkspaceEdit(): Promise { let { editState } = this if (!editState || !editState.applied) { void this.window.showWarningMessage(`No workspace edit to undo`) return } editState.applied = false await this.undoChanges(editState.recovers) } public async redoWorkspaceEdit(): Promise { let { editState } = this if (!editState || editState.applied) { void this.window.showWarningMessage(`No workspace edit to redo`) return } this.editState = undefined await this.applyEdit(editState.edit) } public validateChanges(documentChanges: ReadonlyArray): void { let { documents } = this for (let change of documentChanges) { if (TextDocumentEdit.is(change)) { let { uri, version } = change.textDocument let doc = documents.getDocument(uri) if (typeof version === 'number' && version > 0) { if (!doc) throw errors.notLoaded(uri) if (doc.version != version) throw new Error(`${uri} changed before apply edit`) } else if (!doc && !isFile(uri)) { throw errors.badScheme(uri) } } else if (CreateFile.is(change) || DeleteFile.is(change)) { if (!isFile(change.uri)) throw errors.badScheme(change.uri) } else if (RenameFile.is(change)) { if (!isFile(change.oldUri) || !isFile(change.newUri)) { throw errors.badScheme(change.oldUri) } } } } public async findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Promise { let folders = this.workspaceFolderControl.workspaceFolders if (token?.isCancellationRequested || !folders.length || maxResults === 0) return [] maxResults = maxResults ?? Infinity let roots = folders.map(o => URI.parse(o.uri).fsPath) let pattern: string if (typeof include !== 'string') { pattern = include.pattern roots = [include.baseUri.fsPath] } else { pattern = include } let res: URI[] = [] let exceed = false const ac = new AbortController() if (token) { token.onCancellationRequested(() => { if (!ac.signal.aborted) ac.abort() }) } for (let root of roots) { try { let files = await glob.glob(pattern, { signal: ac.signal, dot: true, cwd: root, nodir: true, absolute: false }) if (token?.isCancellationRequested) break for (let file of files) { if (exclude && fileMatch(root, file, exclude)) continue res.push(URI.file(path.join(root, file))) if (res.length === maxResults) { exceed = true break } } if (exceed) break } catch (e) { if (e['name'] === 'AbortError') { break } } } return res } private async fireWaitUntilEvent(emitter: Emitter, properties: Omit, recovers?: RecoverFunc[]): Promise { let firing = true let promises: Promise[] = [] emitter.fire({ ...properties, waitUntil: thenable => { if (!firing) throw errors.shouldNotAsync('waitUntil') let tp = new Promise(resolve => { setTimeout(resolve, this.operationTimeout) }) let promise = Promise.race([thenable, tp]).then(edit => { if (edit && WorkspaceEdit.is(edit)) { return this.applyEdit(edit, true) } }) promises.push(promise) } } as any) firing = false await Promise.all(promises) } } function fileMatch(root: string, relpath: string, pattern: GlobPattern): boolean { let filepath = path.join(root, relpath) if (typeof pattern !== 'string') { let base = pattern.baseUri.fsPath if (!isParentFolder(base, filepath)) return false let rp = path.relative(base, filepath) return minimatch(rp, pattern.pattern, { dot: true }) } return minimatch(relpath, pattern, { dot: true }) } function fsPath(uri: string): string { return URI.parse(uri).fsPath } ================================================ FILE: src/core/funcs.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import type { DocumentFilter, DocumentSelector } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import Configurations from '../configuration' import Resolver from '../model/resolver' import { isVim } from '../util/constants' import { onUnexpectedError } from '../util/errors' import * as fs from '../util/fs' import { Mutex } from '../util/mutex' import { minimatch, os, path, semver, which } from '../util/node' import * as platform from '../util/platform' import { RelativePattern, TextDocumentFilter } from '../util/protocol' let NAME_SPACE = 2000 const resolver = new Resolver() const namespaceMap: Map = new Map() const mutex: Mutex = new Mutex() export interface PartialEnv { isVim: boolean version: string } /** * Like vim's has(), but for version check only. * Check patch on neovim and check nvim on vim would return false. * * For example: * - has('nvim-0.6.0') * - has('patch-7.4.248') */ export function has(env: PartialEnv, feature: string): boolean { if (!feature.startsWith('nvim-') && !feature.startsWith('patch-')) { throw new Error('Feature param could only starts with nvim and patch') } if (!env.isVim && feature.startsWith('patch-')) { return false } if (env.isVim && feature.startsWith('nvim-')) { return false } if (env.isVim) { let [_, major, minor, patch] = env.version.match(/^(\d)(\d{2})(\d+)$/) let version = `${major}.${parseInt(minor, 10)}.${parseInt(patch, 10)}` return semver.gte(version, convertVersion(feature.slice(6))) } return semver.gte(env.version, feature.slice(5)) } // convert to valid semver version 9.0.0138 to 9.0.138 function convertVersion(version: string): string { let parts = version.split('.') return `${parseInt(parts[0], 10)}.${parseInt(parts[1], 10)}.${parseInt(parts[2], 10)}` } export function callAsync(nvim: Neovim, method: string, args: any[]): Promise { return mutex.use(() => { if (!isVim) return nvim.call(method, args) as Promise return nvim.callAsync('coc#util#with_callback', [method, args]).catch(onUnexpectedError) as Promise }) } /** * @deprecated */ export function createNameSpace(name: string): number { if (namespaceMap.has(name)) return namespaceMap.get(name) NAME_SPACE = NAME_SPACE + 1 namespaceMap.set(name, NAME_SPACE) return NAME_SPACE } /** * Resolve watchman path. */ export function getWatchmanPath(configurations: Configurations): string | null { const watchmanPath = configurations.initialConfiguration.get('coc.preferences.watchmanPath', 'watchman') return which.sync(watchmanPath, { nothrow: true }) } export async function findUp(nvim: Neovim, cwd: string, filename: string | string[]): Promise { let filepath = await nvim.call('coc#util#get_fullpath') as string filepath = path.normalize(filepath) let isFile = filepath && path.isAbsolute(filepath) if (isFile && !fs.isParentFolder(cwd, filepath, true)) { // can't use cwd return fs.findUp(filename, path.dirname(filepath)) } let res = fs.findUp(filename, cwd) if (res && res != os.homedir()) return res if (isFile) return fs.findUp(filename, path.dirname(filepath)) return null } export function resolveModule(name: string): Promise { return resolver.resolveModule(name) } export function score(selector: DocumentSelector | DocumentFilter | string, uri: string, languageId: string, caseInsensitive = platform.isWindows || platform.isMacintosh): number { let u = URI.parse(uri) if (Array.isArray(selector)) { // array -> take max individual value let ret = 0 for (const filter of selector) { const value = score(filter, uri, languageId) if (value === 10) { return value // already at the highest } if (value > ret) { ret = value } } return ret } else if (typeof selector === 'string') { // short-hand notion, desugars to // 'fooLang' -> { language: 'fooLang'} // '*' -> { language: '*' } if (selector === '*') { return 5 } else if (selector === languageId) { return 10 } else { return 0 } } else if (selector && TextDocumentFilter.is(selector)) { // filter -> select accordingly, use defaults for scheme const { language, pattern, scheme } = selector let ret = 0 if (scheme) { if (scheme === u.scheme) { ret = 5 } else if (scheme === '*') { ret = 3 } else { return 0 } } if (language) { if (language === languageId) { ret = 10 } else if (language === '*') { ret = Math.max(ret, 5) } else { return 0 } } if (pattern) { let relativePattern: string if (RelativePattern.is(pattern)) { relativePattern = pattern.pattern let baseUri = URI.parse(typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri) if (u.scheme !== 'file' || !fs.isParentFolder(baseUri.fsPath, u.fsPath, true)) { return 0 } } else { relativePattern = pattern } let p = caseInsensitive ? relativePattern.toLowerCase() : relativePattern let f = caseInsensitive ? u.fsPath.toLowerCase() : u.fsPath if (p === f || minimatch(f, p, { dot: true })) { ret = Math.max(ret, 5) } else { return 0 } } return ret } else { return 0 } } ================================================ FILE: src/core/highlights.ts ================================================ import { Neovim } from '@chemzqm/neovim' import { HighlightItem } from '../types' import { defaultValue } from '../util' import { CancellationToken } from '../util/protocol' export type HighlightItemResult = [string, number, number, number, number?] export type HighlightItemDef = [string, number, number, number, number?, number?, number?] export interface HighlightDiff { remove: number[] removeMarkers: number[] add: HighlightItemDef[] } export function convertHighlightItem(item: HighlightItem): HighlightItemDef { return [item.hlGroup, item.lnum, item.colStart, item.colEnd, item.combine ? 1 : 0, item.start_incl ? 1 : 0, item.end_incl ? 1 : 0] } function isSame(item: HighlightItem, curr: HighlightItemResult): boolean { return curr[0] == item.hlGroup && curr[1] === item.lnum && curr[2] === item.colStart && curr[3] === item.colEnd } export class Highlights { public nvim: Neovim public async diffHighlights(bufnr: number, ns: string, items: HighlightItem[], region?: [number, number], token?: CancellationToken): Promise { let args = [bufnr, ns, Array.isArray(region) ? region[0] : 0, Array.isArray(region) ? region[1] : -1] let curr = await this.nvim.call('coc#highlight#get_highlights', args) as HighlightItemResult[] if (!curr || token?.isCancellationRequested) return null items.sort((a, b) => { if (a.lnum != b.lnum) return a.lnum - b.lnum if (a.colStart != b.colStart) return a.colStart - b.colStart return a.hlGroup > b.hlGroup ? 1 : -1 }) let removeMarkers = [] let newItems: HighlightItemDef[] = [] let itemIndex = 0 let maxIndex = items.length - 1 let maxLnum = 0 // highlights on vim let map: Map = new Map() curr.forEach(o => { maxLnum = Math.max(maxLnum, o[1]) let arr = map.get(o[1]) if (arr) { arr.push(o) } else { map.set(o[1], [o]) } }) if (curr.length > 0) { let start = Array.isArray(region) ? region[0] : 0 for (let i = start; i <= maxLnum; i++) { let exists = defaultValue(map.get(i), []) exists.sort((a, b) => { if (a[2] != b[2]) return a[2] - b[2] return a[0] > b[0] ? 1 : -1 }) let added: HighlightItem[] = [] for (let j = itemIndex; j <= maxIndex; j++) { let o = items[j] if (o.lnum == i) { itemIndex = j + 1 added.push(o) } else { itemIndex = j break } } if (added.length == 0) { removeMarkers.push(...exists.map(o => o[4])) } else { if (exists.length == 0) { newItems.push(...added.map(o => convertHighlightItem(o))) } else { // skip same markers at beginning of exists and removeMarkers let skip = 0 let min = Math.min(exists.length, added.length) while (skip < min) { if (isSame(added[skip], exists[skip])) { skip++ } else { break } } let toRemove = exists.slice(skip).map(o => o[4]) removeMarkers.push(...toRemove) newItems.push(...added.slice(skip).map(o => convertHighlightItem(o))) } } } } for (let i = itemIndex; i <= maxIndex; i++) { newItems.push(convertHighlightItem(items[i])) } return { remove: [], add: newItems, removeMarkers } } public async applyDiffHighlights(bufnr: number, ns: string, priority: number, diff: HighlightDiff, notify: boolean): Promise { let { nvim } = this let { remove, add, removeMarkers } = diff if (remove.length === 0 && add.length === 0 && removeMarkers.length === 0) return nvim.pauseNotification() if (add.length) { nvim.call('coc#highlight#set', [bufnr, ns, add, priority], true) } if (removeMarkers.length) { nvim.call('coc#highlight#del_markers', [bufnr, ns, removeMarkers], true) } if (notify) { nvim.resumeNotification(true, true) } else { await nvim.resumeNotification(true) } } } ================================================ FILE: src/core/keymaps.ts ================================================ 'use strict' import { Neovim, KeymapOption as VimKeymapOption } from '@chemzqm/neovim' import { createLogger } from '../logger' import { KeymapOption } from '../types' import { isVim } from '../util/constants' import { Disposable } from '../util/protocol' import { toBase64 } from '../util/string' const logger = createLogger('core-keymaps') export type MapMode = 'n' | 'i' | 'v' | 'x' | 's' | 'o' | '!' | 't' | 'c' | 'l' export type LocalMode = 'n' | 'i' | 'v' | 's' | 'x' export type KeymapCallback = () => Promise | string | void | Promise export function getKeymapModifier(mode: MapMode, cmd?: boolean): string { if (cmd) return '' if (mode == 'n' || mode == 'o' || mode == 'x' || mode == 'v') return '' if (mode == 'i') return '' if (mode == 's') return '' return '' } export function getBufnr(buffer: number | boolean): number { return typeof buffer === 'number' ? buffer : 0 } export default class Keymaps { private readonly keymaps: Map = new Map() private nvim: Neovim public attach(nvim: Neovim): void { this.nvim = nvim } public async doKeymap(key: string, defaultReturn: string): Promise { let keymap = this.keymaps.get(key) ?? this.keymaps.get('coc-' + key) if (!keymap) { logger.error(`keymap for ${key} not found`) return defaultReturn } let [fn, repeat] = keymap let res = await Promise.resolve(fn()) if (repeat) await this.nvim.command(`silent! call repeat#set("\\(coc-${key})", -1)`) if (res == null) return defaultReturn return res as string } /** * Register global (coc-${key}) key mapping. */ public registerKeymap(modes: MapMode[], name: string, fn: KeymapCallback, opts: KeymapOption = {}): Disposable { if (!name) throw new Error(`Invalid key ${name} of registerKeymap`) let key = `coc-${name}` if (this.keymaps.has(key)) throw new Error(`keymap: "${name}" already exists.`) const lhs = `(${key})` opts = Object.assign({ sync: true, cancel: true, silent: true, repeat: false }, opts) let { nvim } = this this.keymaps.set(key, [fn, !!opts.repeat]) let method = opts.sync ? 'request' : 'notify' for (let mode of modes) { if (mode == 'i') { const cancel = opts.cancel ? 1 : 0 nvim.setKeymap(mode, lhs, `coc#_insert_key('${method}', '${key}', ${cancel})`, { expr: true, noremap: true, silent: opts.silent }) } else { nvim.setKeymap(mode, lhs, `:${getKeymapModifier(mode, opts.cmd)}call coc#rpc#${method}('doKeymap', ['${key}'])`, { noremap: true, silent: opts.silent }) } } return Disposable.create(() => { this.keymaps.delete(key) for (let m of modes) { nvim.deleteKeymap(m, lhs) } }) } public registerExprKeymap(mode: MapMode, lhs: string, fn: KeymapCallback, buffer: number | boolean = false, cancel = true): Disposable { let bufnr = getBufnr(buffer) let id = `${mode}-${toBase64(lhs)}${buffer ? `-${bufnr}` : ''}` let { nvim } = this let rhs: string if (mode == 'i') { rhs = `coc#_insert_key('request', '${id}', ${cancel ? '1' : '0'})` } else { rhs = `coc#rpc#request('doKeymap', ['${id}'])` } let opts = { noremap: true, silent: true, expr: true, nowait: true } if (buffer !== false) { nvim.call('coc#compat#buf_add_keymap', [bufnr, mode, lhs, rhs, opts], true) } else { nvim.setKeymap(mode, lhs, rhs, opts) } this.keymaps.set(id, [fn, false]) return Disposable.create(() => { this.keymaps.delete(id) if (buffer) { nvim.call('coc#compat#buf_del_keymap', [bufnr, mode, lhs], true) } else { nvim.deleteKeymap(mode, lhs) } }) } public registerLocalKeymap(bufnr: number, mode: LocalMode, lhs: string, fn: KeymapCallback, option: boolean | KeymapOption): Disposable { let { nvim } = this let buffer = nvim.createBuffer(bufnr) let id = `local-${bufnr}-${mode}-${toBase64(lhs)}` const opts = toKeymapOption(option) this.keymaps.set(id, [fn, !!opts.repeat]) const method = opts.sync ? 'request' : 'notify' const opt: VimKeymapOption = { noremap: true, silent: opts.silent !== false } if (isVim && opts.special) opt.special = true if (mode == 'i') { const cancel = opts.cancel ? 1 : 0 opt.expr = true buffer.setKeymap(mode, lhs, `coc#_insert_key('${method}', '${id}', ${cancel})`, opt) } else { opt.nowait = true const modify = getKeymapModifier(mode, opts.cmd) buffer.setKeymap(mode, lhs, `:${modify}call coc#rpc#${method}('doKeymap', ['${id}'])`, opt) } return Disposable.create(() => { this.keymaps.delete(id) buffer.deleteKeymap(mode, lhs) }) } } function toKeymapOption(option: KeymapOption | boolean): KeymapOption { const conf = typeof option == 'boolean' ? { sync: !option } : option return Object.assign({ sync: true, cancel: true, silent: true }, conf) } ================================================ FILE: src/core/notifications.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { WorkspaceConfiguration } from '../configuration/types' import Notification, { MessageItem, NotificationConfig, NotificationKind, NotificationPreferences, toButtons, toTitles } from '../model/notification' import ProgressNotification, { formatMessage, Progress } from '../model/progress' import StatusLine from '../model/status' import { defaultValue } from '../util' import { parseExtensionName } from '../util/extensionRegistry' import { toNumber } from '../util/numbers' import { CancellationToken } from '../util/protocol' import { toText } from '../util/string' import { callAsync } from './funcs' import { echoMessages, MsgTypes } from './ui' import { Dialogs } from './dialogs' export type MessageKind = 'Error' | 'Warning' | 'Info' interface NotificationItem { time: string message: string kind: MessageKind } interface NotificationConfiguration { statusLineProgress: boolean border: boolean disabledProgressSources: string[] focusable: boolean highlightGroup: string marginRight: number maxHeight: number maxWidth: number minProgressWidth: number timeout: number winblend: number } /** * Value-object describing where and how progress should show. */ export interface ProgressOptions { /** * A human-readable string which will be used to describe the * operation. */ title?: string /** * Controls if a cancel button should show to allow the user to * cancel the long running operation. */ cancellable?: boolean /** * Extension or language-client id */ source?: string } export class Notifications { public nvim: Neovim public configuration: WorkspaceConfiguration public statusLine: StatusLine private _history: NotificationItem[] = [] constructor(private dialogs: Dialogs) { } private getCurrentTimestamp(): string { const now = new Date() const year = now.getFullYear() const month = (now.getMonth() + 1).toString().padStart(2, '0') const day = now.getDate().toString().padStart(2, '0') const hours = now.getHours().toString().padStart(2, '0') const minutes = now.getMinutes().toString().padStart(2, '0') const seconds = now.getSeconds().toString().padStart(2, '0') const ms = now.getMilliseconds().toString().padStart(3, '0') return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}` } public async _showMessage(kind: MessageKind, message: string, items: T[]): Promise { this._history.push({ time: this.getCurrentTimestamp(), kind, message }) let msgDialogKind = this.messageDialogKind if (this.enableMessageDialog === true) { // maintain backwards compatibility, with the original implementation, set the default message kind to use // notification interface even when action items are present. msgDialogKind = 'notification' } if (items.length > 0) { switch (msgDialogKind) { case 'confirm': return await this.showConfirm(message, items, kind) case 'menu': return await this.showMenuPicker(`Choose an action`, message, `Coc${kind}Float`, items) case 'notification': { let texts = items.map(o => typeof o === 'string' ? o : o.title) let idx = await this.createNotification(kind.toLowerCase() as NotificationKind, message, texts) return items[idx] } default: throw new Error(`Unexpected messageDialogKind: ${this.messageDialogKind}`) } } else { // by default the report kind will be echo, meaning that we still keep backwards compatibility with the original // behavior where the user expects that messages are printed to the echo area, with the added caveat that all // message kinds will go there now, information, warning or error let msgReportKind = this.messageReportKind switch (msgReportKind) { case 'echo': { let msgType: MsgTypes = kind == 'Info' ? 'more' : kind == 'Error' ? 'error' : 'warning' this.echoMessages(message, msgType) break } case 'notification': { await this.createNotification(kind.toLowerCase() as NotificationKind, message, []) break } default: throw new Error(`Unexpected messageReportKind: ${msgReportKind}`) } return undefined } } public get history(): NotificationItem[] { return this._history } public clearHistory(): void { this._history = [] } public createNotification(kind: NotificationKind, message: string, items: string[]): Promise { return new Promise((resolve, reject) => { let config: NotificationConfig = { kind, content: message, buttons: toButtons(items), callback: idx => { resolve(idx) } } let notification = new Notification(this.nvim, config) notification.show(this.getNotificationPreference()).catch(reject) if (items.length == 0) { resolve(-1) } }) } public async showConfirm(message: string, items: T[], kind: MessageKind): Promise { let titles = toTitles(items) let choices = titles.map((s, i) => `${i + 1}${s}`) let res = await callAsync(this.nvim, 'confirm', [message, choices.join('\n'), 1, kind]) as number return items[res - 1] } public async showMenuPicker(title: string, content: string, hlGroup: string, items: T[]): Promise { let texts = items.map(o => typeof o === 'string' ? o : o.title) let res = await this.dialogs.showMenuPicker(texts, { position: 'center', content, title: title.replace(/\r?\n/, ' '), borderhighlight: hlGroup }) return items[res] } public async showNotification(config: NotificationConfig, stack: string): Promise { let notification = new Notification(this.nvim, config) await notification.show(this.getNotificationPreference(stack)) } public echoMessages(msg: string, messageType: MsgTypes): void { let level = this.configuration.get('coc.preferences.messageLevel', 'more') echoMessages(this.nvim, msg, messageType, level) } public async withProgress(options: ProgressOptions, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { let config = this.configuration.get('notification') if (!options.cancellable && config.statusLineProgress) { return await this.createStatusLineProgress(options, task) } let progress = new ProgressNotification(this.nvim, { task, title: options.title, cancellable: options.cancellable }) let minWidth = toNumber(config.minProgressWidth, 40) let promise = new Promise(resolve => { progress.onDidFinish(resolve) }) await progress.show(Object.assign(this.getNotificationPreference(options.source, true), { minWidth })) return await promise } private async createStatusLineProgress(options: ProgressOptions, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { let { title } = options let statusItem = this.statusLine.createStatusBarItem(0, true) statusItem.text = toText(title) statusItem.show() let total = 0 let result = await task({ report: p => { if (p.increment) { total += p.increment } statusItem.text = formatMessage(title, p.message, total).replace(/\r?\n/g, ' ') } }, CancellationToken.None) statusItem.dispose() return result } private get enableMessageDialog(): boolean { return this.configuration.get('coc.preferences.enableMessageDialog', false) } private get messageDialogKind(): string { return this.configuration.get('coc.preferences.messageDialogKind', 'confirm') } private get messageReportKind(): string { return this.configuration.get('coc.preferences.messageReportKind', 'echo') } private getNotificationPreference(source?: string, isProgress = false): NotificationPreferences { if (!source) source = parseExtensionName(Error().stack) let config = this.configuration.get('notification') let disabled = false if (isProgress) { let disabledList = defaultValue(config.disabledProgressSources, []) as string[] disabled = Array.isArray(disabledList) && (disabledList.includes('*') || disabledList.includes(source)) } return { border: config.border, focusable: config.focusable, marginRight: toNumber(config.marginRight, 10), timeout: toNumber(config.timeout, 10000), maxWidth: toNumber(config.maxWidth, 60), maxHeight: toNumber(config.maxHeight, 10), highlight: config.highlightGroup, winblend: toNumber(config.winblend, 30), disabled, source, } } } ================================================ FILE: src/core/terminals.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import { TerminalModel, TerminalOptions } from '../model/terminal' import { disposeAll } from '../util' import { toObject } from '../util/object' import { Disposable, Emitter, Event } from '../util/protocol' export interface TerminalResult { bufnr: number success: boolean content?: string } export interface OpenTerminalOption { /** * Cwd of terminal, default to result of |getcwd()| */ cwd?: string /** * Close terminal on job finish, default to true. */ autoclose?: boolean /** * Keep focus current window, default to false. */ keepfocus?: boolean /** * Position of terminal window, default to 'right'. */ position?: 'bottom' | 'right' } export default class Terminals { private _terminals: Map = new Map() private disposables: Disposable[] = [] private readonly _onDidOpenTerminal = new Emitter() private readonly _onDidCloseTerminal = new Emitter() public readonly onDidCloseTerminal: Event = this._onDidCloseTerminal.event public readonly onDidOpenTerminal: Event = this._onDidOpenTerminal.event constructor() { events.on('BufUnload', bufnr => { if (this._terminals.has(bufnr)) { let terminal = this._terminals.get(bufnr) this._onDidCloseTerminal.fire(terminal) this._terminals.delete(bufnr) } }, null, this.disposables) events.on('TermExit', (bufnr, status) => { let terminal = this._terminals.get(bufnr) if (terminal) { terminal.onExit(status) terminal.dispose() } }, null, this.disposables) } public get terminals(): ReadonlyArray { return Array.from(this._terminals.values()) } public async createTerminal(nvim: Neovim, opts: TerminalOptions): Promise { let cwd = opts.cwd let cmd = opts.shellPath let args = opts.shellArgs if (!cmd) cmd = await nvim.getOption('shell') as string if (!cwd) cwd = await nvim.call('getcwd') as string let terminal = new TerminalModel(cmd, args || [], nvim, opts.name, opts.strictEnv) await terminal.start(cwd, opts.env) this._terminals.set(terminal.bufnr, terminal) this._onDidOpenTerminal.fire(terminal) return terminal } public async runTerminalCommand(nvim: Neovim, cmd: string, cwd: string | undefined, keepfocus: boolean): Promise { return await nvim.callAsync('coc#ui#run_terminal', { cmd, cwd, keepfocus: keepfocus ? 1 : 0 }) as TerminalResult } public async openTerminal(nvim: Neovim, cmd: string, opts?: OpenTerminalOption): Promise { return await nvim.call('coc#ui#open_terminal', { cmd, ...toObject(opts) }) as number } public reset(): void { for (let terminal of this._terminals.values()) { terminal.dispose() } this._terminals.clear() } public dispose(): void { this._onDidOpenTerminal.dispose() this._onDidCloseTerminal.dispose() disposeAll(this.disposables) this.reset() } } ================================================ FILE: src/core/ui.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Position, Range } from 'vscode-languageserver-types' import FloatFactoryImpl, { FloatWinConfig } from '../model/floatFactory' import Regions from '../model/regions' import { Documentation, FloatConfig, FloatFactory, FloatOptions } from '../types' import { isVim } from '../util/constants' import { byteIndex, byteLength } from '../util/string' export interface ScreenPosition { row: number col: number } const operateModes = ['char', 'line', 'block'] export type MsgTypes = 'error' | 'warning' | 'more' export enum MessageLevel { More, Warning, Error } export async function getCursorPosition(nvim: Neovim): Promise { // vim can't count utf16 let [line, content] = await nvim.eval(`[line('.')-1, strpart(getline('.'), 0, col('.') - 1)]`) as [number, string] return Position.create(line, content.length) } export async function getVisibleRanges(nvim: Neovim, bufnr: number, winid?: number): Promise<[number, number][]> { if (winid == null) { const spans = await nvim.call('coc#window#visible_ranges', [bufnr]) as [number, number][] if (spans.length === 0) return [] return Regions.mergeSpans(spans) } const span = await nvim.call('coc#window#visible_range', [winid]) as [number, number] | null return span == null ? [] : [span] } export async function getLineAndPosition(nvim: Neovim): Promise<{ text: string, line: number, character: number }> { let [text, lnum, content] = await nvim.eval(`[getline('.'), line('.'), strpart(getline('.'), 0, col('.') - 1)]`) as [string, number, string] return { text, line: lnum - 1, character: content.length } } export function createFloatFactory(nvim: Neovim, conf: FloatWinConfig, defaults: FloatConfig): FloatFactory { let opts = Object.assign({}, defaults, conf) let factory = new FloatFactoryImpl(nvim) return { get window() { return factory.window }, show: (docs: Documentation[], option?: FloatOptions) => { return factory.show(docs, option ? Object.assign({}, opts, option) : opts) }, activated: () => { return factory.activated() }, dispose: () => { factory.dispose() }, checkRetrigger: bufnr => { return factory.checkRetrigger(bufnr) }, close: () => { factory.close() } } } /** * Prompt user for confirm, a float/popup window would be used when possible, * use vim's |confirm()| function as callback. * @param title The prompt text. * @returns Result of confirm. */ export async function showPrompt(nvim: Neovim, title: string): Promise { let res = await nvim.callAsync('coc#dialog#prompt_confirm', [title]) return res == 1 } /** * Move cursor to position. * @param position LSP position. */ export async function moveTo(nvim: Neovim, position: Position, redraw: boolean): Promise { await nvim.call('coc#cursor#move_to', [position.line, position.character]) if (redraw) nvim.command('redraw', true) } /** * Get current cursor character offset in document, * length of line break would always be 1. * @returns Character offset. */ export async function getOffset(nvim: Neovim): Promise { return await nvim.call('coc#cursor#char_offset') as number } /** * Get screen position of current cursor(relative to editor), * both `row` and `col` are 0 based. * @returns Cursor screen position. */ export async function getCursorScreenPosition(nvim: Neovim): Promise { let [row, col] = await nvim.call('coc#cursor#screen_pos') as [number, number] return { row, col } } export async function echoLines(nvim: Neovim, env: { cmdheight: number, columns: number }, lines: string[], truncate: boolean): Promise { let cmdHeight = env.cmdheight if (lines.length > cmdHeight && truncate) { lines = lines.slice(0, cmdHeight) } let maxLen = env.columns - 12 lines = lines.map(line => { line = line.replace(/\n/g, ' ') if (truncate) line = line.slice(0, maxLen) return line }) if (truncate && lines.length == cmdHeight) { let last = lines[lines.length - 1] lines[cmdHeight - 1] = `${last.length >= maxLen ? last.slice(0, -4) : last} ...` } await nvim.call('coc#ui#echo_lines', [lines]) } /** * Reveal message with highlight. */ export function echoMessages(nvim: Neovim, msg: string, messageType: MsgTypes, messageLevel: string): void { let hl: 'Error' | 'MoreMsg' | 'WarningMsg' = 'Error' let level = MessageLevel.Error switch (messageType) { case 'more': level = MessageLevel.More hl = 'MoreMsg' break case 'warning': level = MessageLevel.Warning hl = 'WarningMsg' break } if (level >= toMessageLevel(messageLevel)) { let method = isVim ? 'callTimer' : 'call' nvim[method]('coc#ui#echo_messages', [hl, ('[coc.nvim] ' + msg).split('\n')], true) } } export function toMessageLevel(level: string): MessageLevel { switch (level) { case 'error': return MessageLevel.Error case 'warning': return MessageLevel.Warning default: return MessageLevel.More } } /** * Mode could be 'char', 'line', 'cursor', 'v', 'V', '\x16' */ export async function getSelection(nvim: Neovim, mode: string): Promise { if (mode === 'currline') { let line = await nvim.call('line', ['.']) as number return Range.create(line - 1, 0, line, 0) } if (mode === 'cursor') { let position = await getCursorPosition(nvim) return Range.create(position, position) } let res = await nvim.call('coc#cursor#get_selection', [operateModes.includes(mode) ? 1 : 0]) if (!res || res[0] == -1) return null return Range.create(res[0], res[1], res[2], res[3]) } export async function selectRange(nvim: Neovim, range: Range, redraw: boolean): Promise { let { start, end } = range let [line, endLine] = await nvim.eval(`[getline(${start.line + 1}),getline(${end.line + 1})]`) as [string, string] let col = line.length > 0 ? byteIndex(line, start.character) : 0 let endCol: number let endLnum: number let toEnd = end.character == 0 if (toEnd) { endLnum = end.line == 0 ? 0 : end.line - 1 let pre = await nvim.call('getline', [endLnum + 1]) as string endCol = byteLength(pre) } else { endLnum = end.line endCol = endLine.length > 0 ? byteIndex(endLine, end.character) : 0 } nvim.pauseNotification() nvim.command(`noa call cursor(${start.line + 1},${col + 1})`, true) nvim.command('normal! v', true) nvim.command(`noa call cursor(${endLnum + 1},${endCol})`, true) if (toEnd) nvim.command('normal! $', true) await nvim.resumeNotification(redraw) } ================================================ FILE: src/core/watchers.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import events from '../events' import { createLogger } from '../logger' import { ProviderResult } from '../provider' import { Env } from '../types' import { disposeAll } from '../util' import { Disposable } from '../util/protocol' import { toErrorText } from '../util/string' const logger = createLogger('watchers') export default class Watchers implements Disposable { private nvim: Neovim private optionCallbacks: Map ProviderResult>> = new Map() private globalCallbacks: Map ProviderResult>> = new Map() private disposables: Disposable[] = [] constructor() { events.on('OptionSet', async (changed: string, oldValue: any, newValue: any) => { let cbs = Array.from(this.optionCallbacks.get(changed) ?? []) await Promise.allSettled(cbs.map(cb => { return (async () => { try { await Promise.resolve(cb(oldValue, newValue)) } catch (e) { this.nvim.errWriteLine(`Error on OptionSet '${changed}': ${toErrorText(e)}`) logger.error(`Error on OptionSet callback:`, e) } })() })) }, null, this.disposables) events.on('GlobalChange', async (changed: string, oldValue: any, newValue: any) => { let cbs = Array.from(this.globalCallbacks.get(changed) ?? []) await Promise.allSettled(cbs.map(cb => { return (async () => { try { await Promise.resolve(cb(oldValue, newValue)) } catch (e) { this.nvim.errWriteLine(`Error on GlobalChange '${changed}': ${toErrorText(e)}`) logger.error(`Error on GlobalChange callback:`, e) } })() })) }, null, this.disposables) } public get options(): string[] { return Array.from(this.optionCallbacks.keys()) } public attach(nvim: Neovim, _env: Env): void { this.nvim = nvim } /** * Watch for option change. */ public watchOption(key: string, callback: (oldValue: any, newValue: any) => Thenable | void, disposables?: Disposable[]): Disposable { let cbs = this.optionCallbacks.get(key) if (!cbs) { cbs = new Set() this.optionCallbacks.set(key, cbs) } cbs.add(callback) let cmd = `autocmd! coc_dynamic_option OptionSet ${key} call coc#rpc#notify('OptionSet',[expand(''), v:option_old, v:option_new])` this.nvim.command(cmd, true) let disposable = Disposable.create(() => { let cbs = this.optionCallbacks.get(key) cbs.delete(callback) if (cbs.size === 0) this.nvim.command(`autocmd! coc_dynamic_option OptionSet ${key}`, true) }) if (disposables) disposables.push(disposable) return disposable } /** * Watch global variable, works on neovim only. */ public watchGlobal(key: string, callback: (oldValue: any, newValue: any) => Thenable | void, disposables?: Disposable[]): Disposable { let { nvim } = this let cbs = this.globalCallbacks.get(key) if (!cbs) { cbs = new Set() this.globalCallbacks.set(key, cbs) } cbs.add(callback) nvim.call('coc#_watch', key, true) let disposable = Disposable.create(() => { let cbs = this.globalCallbacks.get(key) cbs.delete(callback) if (cbs.size === 0) nvim.call('coc#_unwatch', key, true) }) if (disposables) disposables.push(disposable) return disposable } public dispose(): void { disposeAll(this.disposables) } } ================================================ FILE: src/core/watchman.ts ================================================ 'use strict' import type { Client } from 'fb-watchman' import { v1 as uuidv1 } from 'uuid' import { createLogger } from '../logger' import { OutputChannel } from '../types' import { minimatch, path } from '../util/node' import { Disposable } from '../util/protocol' const logger = createLogger('core-watchman') const requiredCapabilities = ['relative_root', 'cmd-watch-project', 'wildmatch', 'field-new'] export interface WatchResponse { warning?: string watcher: string watch: string relative_path?: string } export interface FileChangeItem { size: number name: string exists: boolean new: boolean type: 'f' | 'd' mtime_ms: number } export interface FileChange { root: string subscription: string files: FileChangeItem[] } export type ChangeCallback = (FileChange) => void /** * Watchman wrapper for fb-watchman client * @public */ export default class Watchman { private client: Client private relative_path: string | undefined private _listeners: ((change: FileChange) => void)[] = [] private _root: string public subscription: string | undefined constructor(binaryPath: string, private channel?: OutputChannel) { const watchman = require('fb-watchman') this.client = new watchman.Client({ watchmanBinaryPath: binaryPath }) this.client.setMaxListeners(300) } public get root(): string { return this._root } public checkCapability(): Promise { let { client } = this return new Promise(resolve => { client.capabilityCheck({ optional: [], required: requiredCapabilities }, (error, resp) => { if (error) return resolve(false) let { capabilities } = resp for (let key of Object.keys(capabilities)) { if (!capabilities[key]) return resolve(false) } resolve(true) }) }) } public async watchProject(root: string): Promise { this._root = root let resp = await this.command(['watch-project', root]) let { watch, warning, relative_path } = resp as WatchResponse if (!watch) return false if (warning) { logger.warn(warning) this.appendOutput(warning, 'Warning') } this.relative_path = relative_path logger.info(`watchman watching project: ${root}`) this.appendOutput(`watchman watching project: ${root}`) let { clock } = await this.command(['clock', watch]) let sub: any = { expression: ['allof', ['type', 'f', 'wholename']], fields: ['name', 'size', 'new', 'exists', 'type', 'mtime_ms', 'ctime_ms'], since: clock, } if (relative_path) { sub.relative_root = relative_path root = path.join(watch, relative_path) } let uid = uuidv1() let { subscribe } = await this.command(['subscribe', watch, uid, sub]) this.subscription = subscribe this.appendOutput(`subscribing events in ${root}`) this.client.on('subscription', resp => { if (!resp || resp.subscription != uid || !resp.files) return for (let listener of this._listeners) { // @ts-expect-error file change item listener(resp) } }) return true } private command(args: any[]): Promise { return new Promise((resolve, reject) => { // @ts-expect-error any type this.client.command(args, (error, resp) => { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors if (error) return reject(error) resolve(resp) }) }) } public subscribe(globPattern: string, cb: ChangeCallback): Disposable { let fn = (change: FileChange) => { let { files } = change files = files.filter(f => f.type == 'f' && minimatch(f.name, globPattern, { dot: true })) if (!files.length) return let ev: FileChange = Object.assign({}, change) if (this.relative_path) ev.root = path.resolve(change.root, this.relative_path) this.appendOutput(`file change of "${globPattern}" detected: ${JSON.stringify(ev, null, 2)}`) cb(ev) } this._listeners.push(fn) return { dispose: () => { let idx = this._listeners.indexOf(fn) if (idx !== -1) this._listeners.splice(idx, 1) }, } } public dispose(): void { if (this.client) { this.client.end() this.client = undefined } } private appendOutput(message: string, type = "Info"): void { if (this.channel) { this.channel.appendLine(`[${type} - ${(new Date().toLocaleTimeString())}] ${message}`) } } public static async createClient(binaryPath: string, root: string, channel?: OutputChannel): Promise { let watchman: Watchman try { watchman = new Watchman(binaryPath, channel) let valid = await watchman.checkCapability() if (!valid) throw new Error('required capabilities do not exist.') let watching = await watchman.watchProject(root) if (!watching) throw new Error('unable to watch') return watchman } catch (e) { if (watchman) watchman.dispose() throw e } } } ================================================ FILE: src/core/workspaceFolder.ts ================================================ 'use strict' import type { WorkspaceFoldersChangeEvent } from 'vscode-languageserver-protocol' import { WorkspaceFolder } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import Configurations from '../configuration' import events from '../events' import { createLogger } from '../logger' import Document from '../model/document' import { getConditionValue } from '../util' import { distinct, isFalsyOrEmpty, toArray } from '../util/array' import { isCancellationError } from '../util/errors' import { Extensions as ExtensionsInfo, IExtensionRegistry } from '../util/extensionRegistry' import { checkFolder, isDirectory, isFolderIgnored, isParentFolder, resolveRoot } from '../util/fs' import { path } from '../util/node' import { toObject } from '../util/object' import { CancellationToken, CancellationTokenSource, Emitter, Event } from '../util/protocol' import { Registry } from '../util/registry' import type { LanguageServerConfig } from '../services' export enum PatternType { Buffer, LanguageServer, Global, } const logger = createLogger('core-workspaceFolder') const PatternTypes = [PatternType.Buffer, PatternType.LanguageServer, PatternType.Global] const checkPatternTimeout = getConditionValue(5000, 50) function toWorkspaceFolder(fsPath: string): WorkspaceFolder | undefined { if (!fsPath || !path.isAbsolute(fsPath)) { logger.error(`Invalid folder: ${fsPath}, full path required.`) return undefined } return { name: path.basename(fsPath), uri: URI.file(fsPath).toString() } } const extensionRegistry = Registry.as(ExtensionsInfo.ExtensionContribution) interface WorkspaceConfig { readonly ignoredFiletypes: string[] readonly bottomUpFiletypes: string[] readonly ignoredFolders: string[] readonly workspaceFolderCheckCwd: boolean readonly workspaceFolderFallbackCwd: boolean rootPatterns: string[] } export default class WorkspaceFolderController { public config: WorkspaceConfig private _onDidChangeWorkspaceFolders = new Emitter() public readonly onDidChangeWorkspaceFolders: Event = this._onDidChangeWorkspaceFolders.event // filetype => patterns private rootPatterns: Map = new Map() private _workspaceFolders: WorkspaceFolder[] = [] private _tokenSources: Set = new Set() constructor(private configurations: Configurations) { events.on('VimLeavePre', this.cancelAll, this) this.updateConfiguration() this.updateServerRootPatterns() this.configurations.onDidChange(e => { if (e.affectsConfiguration('workspace') || e.affectsConfiguration('coc.preferences')) { this.updateConfiguration() } if (e.affectsConfiguration('languageserver')) { this.updateServerRootPatterns() } }) } private updateConfiguration(): void { const allConfig = this.configurations.initialConfiguration let config = allConfig.get('workspace') let oldConfig = allConfig.get('coc.preferences.rootPatterns') this.config = { rootPatterns: isFalsyOrEmpty(oldConfig) ? toArray(config.rootPatterns) : oldConfig, ignoredFiletypes: toArray(config.ignoredFiletypes), bottomUpFiletypes: toArray(config.bottomUpFiletypes), ignoredFolders: toArray(config.ignoredFolders), workspaceFolderCheckCwd: !!config.workspaceFolderCheckCwd, workspaceFolderFallbackCwd: !!config.workspaceFolderFallbackCwd } } private updateServerRootPatterns(): void { let lspConfig = this.configurations.getConfiguration('languageserver', null) this.rootPatterns.clear() for (let config of Object.values(toObject>(lspConfig))) { let { filetypes, rootPatterns } = config if (Array.isArray(filetypes) && !isFalsyOrEmpty(rootPatterns)) { filetypes.filter(s => typeof s === 'string').forEach(filetype => { this.addRootPattern(filetype, rootPatterns) }) } } } public cancelAll(): void { for (let tokenSource of this._tokenSources) { tokenSource.cancel() } } public setWorkspaceFolders(folders: string[] | undefined): void { if (!folders || !Array.isArray(folders)) return let arr = folders.filter(f => f.length > 0).map(f => toWorkspaceFolder(f)) this._workspaceFolders = arr.filter(o => o != null) } public getWorkspaceFolder(uri: URI): WorkspaceFolder | undefined { if (uri.scheme !== 'file') return undefined let folders = Array.from(this._workspaceFolders).map(o => URI.parse(o.uri).fsPath) folders.sort((a, b) => b.length - a.length) let fsPath = uri.fsPath let folder = folders.find(f => isParentFolder(f, fsPath, true)) return toWorkspaceFolder(folder) } public getRelativePath(pathOrUri: string | URI, includeWorkspace?: boolean): string { let resource: URI | undefined let p = '' if (typeof pathOrUri === 'string') { resource = URI.file(pathOrUri) p = pathOrUri } else if (pathOrUri != null) { resource = pathOrUri p = pathOrUri.fsPath } if (!resource) return p const folder = this.getWorkspaceFolder(resource) if (!folder) return p if (typeof includeWorkspace === 'undefined' && this._workspaceFolders) { includeWorkspace = this._workspaceFolders.length > 1 } let result = path.relative(URI.parse(folder.uri).fsPath, resource.fsPath) result = result == '' ? resource.fsPath : result if (includeWorkspace && folder.name) { result = `${folder.name}/${result}` } return result! } public get workspaceFolders(): ReadonlyArray { return this._workspaceFolders } public addRootPattern(filetype: string, rootPatterns: string[]): void { let patterns = this.rootPatterns.get(filetype) ?? [] for (let p of rootPatterns) { if (!patterns.includes(p)) { patterns.push(p) } } this.rootPatterns.set(filetype, patterns) } public resolveRoot(document: Document, cwd: string, fireEvent: boolean, expand: ((input: string) => string)): string | null { if (document.buftype !== '' || document.schema !== 'file') return null let u = URI.parse(document.uri) let dir = isDirectory(u.fsPath) ? path.normalize(u.fsPath) : path.dirname(u.fsPath) let { ignoredFiletypes, ignoredFolders, workspaceFolderCheckCwd, workspaceFolderFallbackCwd, bottomUpFiletypes } = this.config if (ignoredFiletypes?.includes(document.filetype)) return null ignoredFolders = Array.isArray(ignoredFolders) ? ignoredFolders.filter(s => s && s.length > 0).map(s => expand(s)) : [] let res: string | null = null for (let patternType of PatternTypes) { let patterns = this.getRootPatterns(document, patternType) if (patterns && patterns.length) { let isBottomUp = bottomUpFiletypes.includes('*') || bottomUpFiletypes.includes(document.filetype) let root = resolveRoot(dir, patterns, cwd, isBottomUp, workspaceFolderCheckCwd, ignoredFolders) if (root) { res = root break } } } if (!res && workspaceFolderFallbackCwd && !isFolderIgnored(cwd, ignoredFolders) && isParentFolder(cwd, dir, true)) { res = cwd } if (res) this.addWorkspaceFolder(res, fireEvent) return res } public addWorkspaceFolder(folder: string, fireEvent: boolean): WorkspaceFolder | undefined { let workspaceFolder: WorkspaceFolder = toWorkspaceFolder(folder) if (!workspaceFolder) return undefined if (this._workspaceFolders.findIndex(o => o.uri == workspaceFolder.uri) == -1) { this._workspaceFolders.push(workspaceFolder) if (fireEvent) { this._onDidChangeWorkspaceFolders.fire({ added: [workspaceFolder], removed: [] }) } } return workspaceFolder } public renameWorkspaceFolder(oldPath: string, newPath: string): void { let added: WorkspaceFolder = toWorkspaceFolder(newPath) if (!added) return let idx = this._workspaceFolders.findIndex(f => URI.parse(f.uri).fsPath == oldPath) if (idx == -1) return let removed = this.workspaceFolders[idx] this._workspaceFolders.splice(idx, 1, added) this._onDidChangeWorkspaceFolders.fire({ removed: [removed], added: [added] }) } public removeWorkspaceFolder(fsPath: string): void { let removed = toWorkspaceFolder(fsPath) if (!removed) return let idx = this._workspaceFolders.findIndex(f => f.uri == removed.uri) if (idx == -1) return this._workspaceFolders.splice(idx, 1) this._onDidChangeWorkspaceFolders.fire({ removed: [removed], added: [] }) } public onDocumentDetach(uris: URI[]): void { let shouldCheck = this.configurations.initialConfiguration.get('workspace.removeEmptyWorkspaceFolder', false) if (!shouldCheck) return let filepaths: string[] = [] for (const uri of uris) { if (uri.scheme === 'file') { filepaths.push(uri.fsPath) } } for (const item of this.workspaceFolders) { const folder = URI.parse(item.uri).fsPath if (!filepaths.some(f => isParentFolder(folder, f))) { this.removeWorkspaceFolder(folder) return } } } public getRootPatterns(document: Document, patternType: PatternType): ReadonlyArray { if (patternType == PatternType.Buffer) return document.getVar('root_patterns', []) if (patternType == PatternType.LanguageServer) return this.getServerRootPatterns(document.languageId) return this.config.rootPatterns } public reset(): void { this.rootPatterns.clear() this._workspaceFolders = [] } /** * Get rootPatterns of filetype by languageserver configuration and extension configuration. */ public getServerRootPatterns(filetype: string): string[] { let patterns = extensionRegistry.getRootPatternsByFiletype(filetype) patterns = patterns.concat(toArray(this.rootPatterns.get(filetype))) return distinct(patterns) } public checkFolder(dir: string, patterns: string[], token?: CancellationToken): Promise { return checkFolder(dir, patterns, token) } public async checkPatterns(folders: ReadonlyArray, patterns: string[]): Promise { if (isFalsyOrEmpty(folders)) return false let dirs = folders.map(f => URI.parse(f.uri).fsPath) let find = false let tokenSource = new CancellationTokenSource() this._tokenSources.add(tokenSource) let token = tokenSource.token let timer = setTimeout(() => { tokenSource.cancel() }, checkPatternTimeout) let results = await Promise.allSettled(dirs.map(dir => { return this.checkFolder(dir, patterns, token).then(checked => { this._tokenSources.delete(tokenSource) if (checked) { find = true clearTimeout(timer) tokenSource.cancel() } }) })) clearTimeout(timer) results.forEach(res => { if (res.status === 'rejected' && !isCancellationError(res.reason)) { logger.error(`checkPatterns error:`, patterns, res.reason) } }) return find } } ================================================ FILE: src/cursors/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Range } from 'vscode-languageserver-types' import commands from '../commands' import Document from '../model/document' import { IConfigurationChangeEvent } from '../types' import { Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import CursorSession, { CursorsConfig } from './session' import { getVisualRanges, splitRange } from './util' export type CursorPosition = [number, number, number, number] export default class Cursors { private sessionsMap: Map = new Map() private disposables: Disposable[] = [] private config: CursorsConfig constructor(private nvim: Neovim) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) workspace.onDidCloseTextDocument(e => { let session = this.getSession(e.bufnr) if (!session) return this.sessionsMap.delete(e.bufnr) session.dispose() }, null, this.disposables) this.disposables.push(commands.registerCommand('editor.action.addRanges', async (ranges: Range[]) => { await this.addRanges(ranges) }, null, true)) } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('cursors')) { let config = workspace.initialConfiguration this.config = config.get('cursors') } } public cancel(uri: number | string): void { let doc = workspace.getDocument(uri) if (!doc) return let session = this.getSession(doc.bufnr) if (session) session.cancel() } public getSession(bufnr: number): CursorSession | undefined { return this.sessionsMap.get(bufnr) } public async isActivated(): Promise { let bufnr = await this.nvim.call('bufnr', ['%']) as number return this.sessionsMap.get(bufnr) != null } public async select(bufnr: number, kind: string, mode: string): Promise { let doc = workspace.getAttachedDocument(bufnr) let { nvim } = this let session = this.createSession(doc) let range: Range if (kind == 'operator') { let res = await nvim.eval(`[getpos("'["),getpos("']")]`) as [CursorPosition, CursorPosition] if (mode == 'char') { let start = doc.getPosition(res[0][1], res[0][2]) let end = doc.getPosition(res[1][1], res[1][2] + 1) let ranges = splitRange(doc, Range.create(start, end)) session.addRanges(ranges) } else { let ranges: Range[] = [] for (let i = res[0][1] - 1; i <= res[1][1] - 1; i++) { let line = doc.getline(i) ranges.push(Range.create(i, 0, i, line.length)) } session.addRanges(ranges) } } else if (kind == 'word') { let pos = await window.getCursorPosition() range = doc.getWordRangeAtPosition(pos) if (!range) { let line = doc.getline(pos.line) if (pos.character == line.length) { range = Range.create(pos.line, Math.max(0, line.length - 1), pos.line, line.length) } else { range = Range.create(pos.line, pos.character, pos.line, pos.character + 1) } } session.addRange(range) await nvim.command(`silent! call repeat#set("\\(coc-cursors-${kind})", -1)`) } else if (kind == 'position') { let pos = await window.getCursorPosition() // make sure range contains character for highlight let line = doc.getline(pos.line) if (pos.character >= line.length) { range = Range.create(pos.line, Math.max(0, line.length - 1), pos.line, line.length) } else { range = Range.create(pos.line, pos.character, pos.line, pos.character + 1) } session.addRange(range) await nvim.command(`silent! call repeat#set("\\(coc-cursors-${kind})", -1)`) } else if (kind == 'range') { await nvim.call('eval', 'feedkeys("\\", "in")') let range = await window.getSelectedRange(mode) if (range) { let ranges = mode == '\x16' ? getVisualRanges(doc, range) : splitRange(doc, range) for (let r of ranges) { session.addRange(r) } } } else { throw new Error(`select kind "${kind}" not supported`) } session.checkRanges() } public createSession(doc: Document): CursorSession { let { bufnr } = doc let session = this.getSession(bufnr) if (session) return session session = new CursorSession(this.nvim, doc, this.config) this.sessionsMap.set(bufnr, session) session.onDidCancel(() => { session.dispose() this.sessionsMap.delete(bufnr) }) return session } // Add ranges to current document public async addRanges(ranges: Range[]): Promise { let { nvim } = this let bufnr = await nvim.call('bufnr', ['%']) as number let doc = workspace.getAttachedDocument(bufnr) let session = this.createSession(doc) return session.addRanges(ranges) } public reset(): void { for (let session of this.sessionsMap.values()) { session.cancel() } this.sessionsMap.clear() } } ================================================ FILE: src/cursors/session.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { TextDocument } from 'vscode-languageserver-textdocument' import { Range, TextEdit } from 'vscode-languageserver-types' import { createLogger } from '../logger' import Document from '../model/document' import { DidChangeTextDocumentParams, HighlightItem } from '../types' import { disposeAll } from '../util' import { fastDiff } from '../util/node' import { comparePosition, emptyRange, rangeAdjacent, rangeInRange, rangeIntersect, rangeOverlap } from '../util/position' import { Disposable, Emitter, Event } from '../util/protocol' import { lineCountChange } from '../util/textedit' import window from '../window' import workspace from '../workspace' import TextRange from './textRange' import { getBeforeCount, getChange, getDelta, SurroundChange, TextChange } from './util' const logger = createLogger('cursors-session') export interface CursorsConfig { cancelKey: string previousKey: string nextKey: string wrapscan: boolean } export interface DiffItem { offset: number add?: string remove?: string } /** * Cursor session for single buffer */ export default class CursorSession { private readonly _onDidCancel = new Emitter() private readonly _onDidUpdate = new Emitter() public readonly onDidCancel: Event = this._onDidCancel.event public readonly onDidUpdate: Event = this._onDidUpdate.event private disposables: Disposable[] = [] private ranges: TextRange[] = [] private activated = true private changing = false constructor(private nvim: Neovim, private doc: Document, private config: CursorsConfig) { let { bufnr } = doc doc.buffer.setVar('coc_cursors_activated', 1, true) let { cancelKey, nextKey, previousKey } = this.config this.disposables.push(workspace.registerLocalKeymap(bufnr, 'n', cancelKey, () => { this.cancel() })) this.disposables.push(workspace.registerLocalKeymap(bufnr, 'n', nextKey, async () => { let ranges = this.ranges.map(o => o.range) let curr = await window.getCursorPosition() for (let r of ranges) { if (comparePosition(r.start, curr) > 0) { await window.moveTo(r.start) return } } let wrap = this.config.wrapscan if (ranges.length && wrap) await window.moveTo(ranges[0].start) })) this.disposables.push(workspace.registerLocalKeymap(bufnr, 'n', previousKey, async () => { let ranges = this.ranges.map(o => o.range) let curr = await window.getCursorPosition() for (let i = ranges.length - 1; i >= 0; i--) { let r = ranges[i] if (comparePosition(r.end, curr) < 0) { await window.moveTo(r.start) return } } let wrap = this.config.wrapscan if (ranges.length && wrap) await window.moveTo(ranges[ranges.length - 1].start) })) this.doc.onDocumentChange(async e => { await this.onChange(e) if (this.activated && !this.changing) { this._onDidUpdate.fire() } }, this, this.disposables) } public checkRanges(): void { if (this.ranges.length == 0) { this.cancel() } } /** * Add or remove range. */ public addRange(range: Range): void { let { ranges } = this let idx = ranges.findIndex(o => rangeIntersect(o.range, range)) // remove range when intersect if (idx !== -1) { ranges.splice(idx, 1) } else { this.createRange(range) ranges.sort((a, b) => comparePosition(a.range.start, b.range.start)) } if (this.ranges.length == 0) { this.cancel() } else { this.doHighlights() } } public addRanges(ranges: Range[]): boolean { this.doc._forceSync() // filter overlap ranges this.ranges = this.ranges.filter(r => { return !ranges.some(range => rangeOverlap(range, r.range)) }) for (let range of ranges) { this.createRange(range) } this.ranges.sort((a, b) => comparePosition(a.range.start, b.range.start)) this.doHighlights() return true } private createRange(range: Range): void { let { textDocument } = this.doc let { line, character } = range.start let text = textDocument.getText(range) this.ranges.push(new TextRange(line, character, text)) } public async onChange(e: DidChangeTextDocumentParams): Promise { if (!this.activated || this.changing) return if (e.contentChanges.length === 0) { this.doHighlights() return } let change = e.contentChanges[0] let { text, range } = change let affected = this.ranges.filter(r => { if (!rangeIntersect(range, r.range)) return false if (rangeAdjacent(range, r.range)) { if (text.includes('\n') || !emptyRange(range)) return false } return true }) if (emptyRange(range) && affected.length > 0) { affected = affected.slice(0, 1) } if (affected.length == 0) { logger.debug('no affected ranges') this.ranges.forEach(r => { r.adjustFromEdit({ range, newText: text }) }) this.doHighlights() } else if (affected.length == 1 && rangeInRange(range, affected[0].range)) { logger.debug('affected single range') if (text.includes('\n')) { this.cancel() return } // change textRange await this.applySingleEdit(affected[0], { range, newText: text }) } else if (!text.length || !this.validChange(range, text)) { logger.debug('filter affected ranges.') let ranges = this.ranges.filter(r => !affected.includes(r)) if (ranges.length > 0) { this.ranges = ranges ranges.forEach(r => { r.adjustFromEdit({ range, newText: text }) }) this.doHighlights() } else { this.cancel() } } else { logger.debug('Check undo & redo') let first = this.ranges[0] let last = this.ranges[this.ranges.length - 1] let originalLines = e.originalLines.slice(first.line, last.line + 1) let newLines = this.doc.textDocument.lines.slice(first.line, last.line + 1) this.applyComposedEdit(originalLines, newLines) } } public validChange(range: Range, text: string): boolean { if (lineCountChange(TextEdit.replace(range, text)) != 0) return false if (!rangeInRange(range, this.range)) return false let first = this.ranges[0] let last = this.ranges[this.ranges.length - 1] if (range.start.line != first.position.line || range.end.line != last.position.line) return false return true } public get range(): Range { let first = this.ranges[0] let last = this.ranges[this.ranges.length - 1] return Range.create(first.position, last.range.end) } private doHighlights(): void { let { nvim, ranges, doc } = this let buffer = doc.buffer let items: HighlightItem[] = [] ranges.forEach(r => { doc.addHighlights(items, 'CocCursorRange', r.range, { combine: false, start_incl: true, end_incl: true }) }) items.sort((a, b) => { if (a.lnum != b.lnum) return a.lnum - b.lnum return a.colStart - b.colStart }) buffer.updateHighlights('cursors', items, { priority: 4096 }) nvim.redrawVim() } public get currentRanges(): Range[] { return this.ranges.map(r => r.range) } /** * Cancel session and highlights */ public cancel(): void { if (!this.activated) return logger.debug('cursors cancel') let buffer = this.doc.buffer this.activated = false this.ranges = [] buffer.clearNamespace('cursors') buffer.setVar('coc_cursors_activated', 0, true) this._onDidUpdate.fire() this._onDidCancel.fire() } /** * Called on buffer unload or cancel */ public dispose(): void { if (!this.doc) return this._onDidCancel.dispose() this._onDidUpdate.dispose() disposeAll(this.disposables) this.ranges = [] this.doc = null } private async applySingleEdit(textRange: TextRange, edit: TextEdit): Promise { // single range change, calculate & apply changes for all ranges let { doc, ranges } = this let after = ranges.filter(r => r !== textRange && r.position.line == textRange.position.line) after.forEach(r => r.adjustFromEdit(edit)) let change = getChange(textRange, edit.range, edit.newText) let delta = getDelta(change) ranges.forEach(r => r.applyChange(change)) let edits = ranges.filter(r => r !== textRange).map(o => o.textEdit) this.changing = true await doc.applyEdits(edits, true, true) this.changing = false if (delta != 0) { for (let r of ranges) { let n = getBeforeCount(r, this.ranges, textRange) r.move(n * delta) } } this.doHighlights() } public applyComposedEdit(originalLines: string[], newLines: string[]): boolean { // check complex edit let diffs = fastDiff(originalLines[0], newLines[0]) let first = this.ranges[0] // let ranges = this.ranges.filter(o => o.line == first.line) let s = first.position.character let firstLine = first.position.line let len = first.text.length let diff = diffs[0] if (s > 0 && (diff[0] != fastDiff.EQUAL || !diff[1].startsWith(originalLines[0].slice(0, s)))) { this.cancel() return false } let used = 0 let invalid = false let changes: DiffItem[] = [] for (let i = 0; i < diffs.length; i++) { let [kind, text] = diffs[i] if (i == 0 && s > 0) { text = text.slice(s) } if (kind == fastDiff.EQUAL) { used += text.length if (used > len) break } else if (kind == fastDiff.DELETE) { let offset = used used += text.length if (used > len) { invalid = true break } changes.push({ offset, remove: text }) } else { let prev = diffs[i - 1] if (prev && prev[0] == fastDiff.DELETE) { changes[changes.length - 1].add = text } else { changes.push({ offset: used, add: text }) } } } if (invalid || !changes.length) { this.cancel() return false } let doc = TextDocument.create('file:///1', '', 0, originalLines.join('\n')) let change: TextChange | SurroundChange if (changes.length == 1) { change = { offset: changes[0].offset, remove: changes[0].remove ? changes[0].remove.length : 0, insert: changes[0].add ?? '' } } else if (surroundChanges(changes, len)) { change = { prepend: [changes[0].remove ? changes[0].remove.length : 0, changes[0].add ?? ''], append: [changes[1].remove ? changes[1].remove.length : 0, changes[1].add ?? ''], remove: false } } else { let text = first.text let oldText = '' let newText = '' let offset = changes[0].offset for (let c of changes) { if (c.offset > offset + oldText.length) { let s = text.slice(offset + oldText.length, c.offset) oldText += s newText += s } if (c.add) { newText += c.add } if (c.remove) { oldText += c.remove } } change = { offset, remove: oldText.length, insert: newText } } let edits: TextEdit[] = this.ranges.map(o => { let line = o.position.line - firstLine let { start, end } = o.range let range = Range.create(line, start.character, line, end.character) o.applyChange(change) return TextEdit.replace(range, o.text) }) let content = TextDocument.applyEdits(doc, edits) if (content !== newLines.join('\n')) { this.cancel() return false } let delta = getDelta(change) if (delta != 0) { for (let r of this.ranges) { let n = getBeforeCount(r, this.ranges) r.move(n * delta) } } this.doHighlights() return true } } export function surroundChanges(changes: DiffItem[], len: number): boolean { if (changes.length != 2 || changes[0].offset != 0) return false let end = changes[1].offset + (changes[1].remove ? changes[1].remove.length : 0) if (end !== len) return false return true } ================================================ FILE: src/cursors/textRange.ts ================================================ 'use strict' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import { getEnd } from '../util/position' import { getChangedPosition } from '../util/textedit' import { isSurroundChange, SurroundChange, TextChange } from './util' export default class TextRange { private start: Position private end: Position private _text: string constructor(line: number, character: number, text: string) { this.start = Position.create(line, character) this._text = text this.end = getEnd(this.start, this._text) } public get position(): Position { return this.start } public get line(): number { return this.start.line } public get text(): string { return this._text } public get range(): Range { return Range.create(this.start, this.end) } public get textEdit(): TextEdit { return { range: this.range, newText: this.text } } public applyChange(change: SurroundChange | TextChange): void { if (isSurroundChange(change)) { this.applySurroundChange(change) } else { this.applyTextChange(change) } } public applySurroundChange(change: SurroundChange): void { let { prepend, append, remove } = change if (remove) { this._text = '' } else { let len = this._text.length let text = this._text.substring(prepend[0], len - append[0]) this._text = `${prepend[1]}${text}${append[1]}` } } public applyTextChange(change: TextChange): void { let { text } = this let { offset, remove, fromEnd, insert } = change if (fromEnd) offset = -offset let pre = text.slice(0, fromEnd && offset == 0 ? text.length : offset) let after = text.slice(pre.length) if (remove) after = after.slice(remove) this._text = `${pre}${insert || ''}${after}` } /** * Adjust range */ public move(delta: number): void { if (delta != 0) { let { line, character } = this.start this.start = Position.create(line, character + delta) } this.end = getEnd(this.start, this._text) } public adjustFromEdit(edit: TextEdit): number { let changed = getChangedPosition(this.start, edit) if (changed.line || changed.character) { let { line, character } = this.start this.start = Position.create(line + changed.line, character + changed.character) this.end = getEnd(this.start, this._text) } return changed.character } public isBefore(range: TextRange): boolean { let { position } = range let { line, character } = this.start return position.line == line && position.character > character } } ================================================ FILE: src/cursors/util.ts ================================================ 'use strict' import { Range } from 'vscode-languageserver-types' import Document from '../model/document' import { equals } from '../util/object' import { toText } from '../util/string' import { getWellformedRange } from '../util/textedit' import type TextRange from './textRange' export interface TextChange { offset: number remove: number insert: string fromEnd?: boolean } export interface SurroundChange { /** * delete count & insert text */ prepend: [number, string] /** * delete count & insert text */ append: [number, string] /** * Remove the range when true */ remove: boolean } /** * Split to single line ranges */ export function splitRange(doc: Document, range: Range): Range[] { let splited: Range[] = [] for (let i = range.start.line; i <= range.end.line; i++) { let curr = toText(doc.getline(i)) let sc = i == range.start.line ? range.start.character : 0 let ec = i == range.end.line ? range.end.character : curr.length if (sc == ec) continue splited.push(Range.create(i, sc, i, ec)) } return splited } /** * Get ranges of visual block */ export function getVisualRanges(doc: Document, range: Range): Range[] { let { start, end } = getWellformedRange(range) let sc = start.character < end.character ? start.character : end.character let ec = start.character < end.character ? end.character : start.character let ranges: Range[] = [] for (let i = start.line; i <= end.line; i++) { let line = doc.getline(i) ranges.push(Range.create(i, sc, i, Math.min(line.length, ec))) } return ranges } export function isSurroundChange(change: TextChange | SurroundChange): change is SurroundChange { return Array.isArray(change['prepend']) && Array.isArray(change['append']) } export function isTextChange(change: TextChange | SurroundChange): change is TextChange { return typeof change['offset'] === 'number' && typeof change['remove'] === 'number' } export function getDelta(change: TextChange | SurroundChange): number { if (isSurroundChange(change)) { return change.append[1].length + change.prepend[1].length - change.append[0] - change.prepend[0] } return change.insert.length - change.remove } export function getChange(r: TextRange, range: Range, newText: string): TextChange | SurroundChange { let text = r.text if (equals(r.range, range)) { // surround let idx = text.indexOf(newText) if (idx !== -1) { let prepend: [number, string] = [idx, ''] let n = text.length - newText.length - idx let append: [number, string] = [n, ''] return { prepend, append, remove: r.text.length === n } } idx = newText.indexOf(text) if (idx !== -1) { let prepend: [number, string] = [0, newText.slice(0, idx)] let append: [number, string] = [0, newText.slice(- (newText.length - text.length - idx))] return { prepend, append, remove: false } } } if (equals(r.range.end, range.end)) { // end change let remove = range.end.character - range.start.character return { offset: remove, remove, insert: newText, fromEnd: true } } let remove = range.end.character - range.start.character let offset = range.start.character - r.range.start.character return { offset, remove, insert: newText } } export function getBeforeCount(textRange: TextRange, ranges: TextRange[], exclude?: TextRange): number { let n = 0 for (let idx = 0; idx < ranges.length; idx++) { const r = ranges[idx] if (r.position.line < textRange.position.line || r === exclude) continue if (r.isBefore(textRange)) { n++ continue } break } return n } ================================================ FILE: src/diagnostic/buffer.ts ================================================ 'use strict' import type { Buffer, Neovim, VirtualTextOption } from '@chemzqm/neovim' import { Diagnostic, DiagnosticSeverity, Location, Position, TextEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import events from '../events' import { SyncItem } from '../model/bufferSync' import Document from '../model/document' import { DiagnosticWithFileType, DidChangeTextDocumentParams, Documentation, FloatFactory, HighlightItem } from '../types' import { getConditionValue } from '../util' import { stripAnsiColoring } from '../util/ansiparse' import { isFalsyOrEmpty } from '../util/array' import { onUnexpectedError } from '../util/errors' import { path } from '../util/node' import { lineInRange, positionInRange } from '../util/position' import { Emitter, Event } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { DiagnosticItem } from './manager' import { adjustDiagnostics, DiagnosticConfig, formatDiagnostic, getHighlightGroup, getLocationListItem, getNameFromSeverity, getSeverityName, getSeverityType, LocationListItem, severityLevel, sortDiagnostics } from './util' const signGroup = 'CocDiagnostic' const NAMESPACE = 'diagnostic' // higher priority first const hlGroups = ['CocErrorHighlight', 'CocWarningHighlight', 'CocInfoHighlight', 'CocHintHighlight', 'CocDeprecatedHighlight', 'CocUnusedHighlight'] interface DiagnosticInfo { /** * current bufnr */ bufnr: number lnum: number winid: number locationlist: string } interface SignItem { name: string lnum: number priority?: number } const delay = getConditionValue(100, 10) const aleMethod = getConditionValue('ale#other_source#ShowResults', 'MockAleResults') let virtualTextSrcId: number | undefined let floatFactory: FloatFactory /** * Manage diagnostics of buffer, including: * * - highlights * - variable * - signs * - location list * - virtual text */ export class DiagnosticBuffer implements SyncItem { private diagnosticsMap: Map> = new Map() private _disposed = false private _dirties: Set = new Set() private _refreshing = false private _config: DiagnosticConfig public refreshHighlights: (() => void) & { clear(): void } private readonly _onDidRefresh = new Emitter>() public readonly onDidRefresh: Event> = this._onDidRefresh.event constructor( private readonly nvim: Neovim, public readonly doc: Document ) { this.loadConfiguration() let timer: NodeJS.Timeout let fn: any = () => { clearTimeout(timer) this._refreshing = true timer = setTimeout(() => { this._refreshing = false if (!this._autoRefresh) return void this._refresh(true) }, delay) } fn.clear = () => { this._refreshing = false clearTimeout(timer) } this.refreshHighlights = fn } private get _autoRefresh(): boolean { return this.config.enable && this.config.autoRefresh && this.dirty && !this.doc.hasChanged } public get config(): Readonly { return this._config } public loadConfiguration(): void { let config = workspace.getConfiguration('diagnostic', this.doc) let changed = this._config && config.enable != this._config.enable this._config = { enable: config.get('enable', true), floatConfig: config.get('floatConfig', {}), messageTarget: config.get('messageTarget', 'float'), enableHighlightLineNumber: config.get('enableHighlightLineNumber', true), highlightLimit: config.get('highlightLimit', 1000), highlightPriority: config.get('highlightPriority'), autoRefresh: config.get('autoRefresh', true), checkCurrentLine: config.get('checkCurrentLine', false), enableSign: workspace.env.sign && config.get('enableSign', true), locationlistUpdate: config.get('locationlistUpdate', true), enableMessage: config.get('enableMessage', 'always'), virtualText: config.get('virtualText', false), virtualTextAlign: config.get('virtualTextAlign', 'after'), virtualTextWinCol: config.get('virtualTextWinCol', null), virtualTextCurrentLineOnly: config.get('virtualTextCurrentLineOnly'), virtualTextPrefix: config.get('virtualTextPrefix', " "), virtualTextFormat: config.get('virtualTextFormat', "%message"), virtualTextLimitInOneLine: config.get('virtualTextLimitInOneLine', 999), virtualTextLineSeparator: config.get('virtualTextLineSeparator', " \\ "), virtualTextLines: config.get('virtualTextLines', 3), displayByAle: config.get('displayByAle', false), displayByVimDiagnostic: config.get('displayByVimDiagnostic', false), level: severityLevel(config.get('level', 'hint')), locationlistLevel: severityLevel(config.get('locationlistLevel')), signLevel: severityLevel(config.get('signLevel')), virtualTextLevel: severityLevel(config.get('virtualTextLevel')), messageLevel: severityLevel(config.get('messageLevel')), signPriority: config.get('signPriority', 10), refreshOnInsertMode: config.get('refreshOnInsertMode', false), filetypeMap: config.get('filetypeMap', {}), showUnused: config.get('showUnused', true), showDeprecated: config.get('showDeprecated', true), format: config.get('format', '[%source%code] [%severity] %message'), showRelatedInformation: config.get('showRelatedInformation', true), } if (this._config.virtualText && !virtualTextSrcId) { void this.nvim.createNamespace('coc-diagnostic-virtualText').then(id => { virtualTextSrcId = id }) } if (changed) { if (this.config.enable) { void this._refresh(false) } else { this.clear() } } } public async setState(enable: boolean): Promise { let curr = this._config.enable if (curr == enable) return this._config.enable = enable if (enable) { await this._refresh(false) } else { this.clear() } } public get dirty(): boolean { return this._dirties.size > 0 } public get bufnr(): number { return this.doc.bufnr } public get uri(): string { return this.doc.uri } public onChange(e: DidChangeTextDocumentParams): void { let changes = e.contentChanges if (changes.length > 0) { let edit = TextEdit.replace(changes[0].range, changes[0].text) for (let [collection, diagnostics] of this.diagnosticsMap.entries()) { let arr = adjustDiagnostics(diagnostics, edit) this.diagnosticsMap.set(collection, arr) } this._dirties = new Set(this.diagnosticsMap.keys()) } if (!this.config.autoRefresh) return this.refreshHighlights() } public onTextChange(): void { this._dirties = new Set(this.diagnosticsMap.keys()) this.refreshHighlights.clear() } private get displayByAle(): boolean { return this._config.displayByAle } private get displayByVimDiagnostic(): boolean { return this.nvim.isVim === false && this._config.displayByVimDiagnostic } public clearHighlight(collection: string): void { this.buffer.clearNamespace(NAMESPACE + collection) } private clearSigns(collection: string): void { this.buffer.unplaceSign({ group: signGroup + collection }) } private get diagnostics(): Diagnostic[] { let res: Diagnostic[] = [] for (let diags of this.diagnosticsMap.values()) { res.push(...diags) } return res } private get buffer(): Buffer { return this.nvim.createBuffer(this.bufnr) } private refreshAle(collection: string, diagnostics: ReadonlyArray): void { let aleItems = diagnostics.map(o => { let range = o.range return { text: o.message, code: o.code, lnum: range.start.line + 1, col: range.start.character + 1, end_lnum: range.end.line + 1, end_col: range.end.character, type: getSeverityType(o.severity) } }) this.nvim.call(aleMethod, [this.bufnr, 'coc' + collection, aleItems], true) } /** * Update diagnostics when diagnostics change on collection. * @param {string} collection * @param {Diagnostic[]} diagnostics */ public async update(collection: string, diagnostics: ReadonlyArray): Promise { let { diagnosticsMap } = this let curr = diagnosticsMap.get(collection) if (!this._dirties.has(collection) && isFalsyOrEmpty(diagnostics) && isFalsyOrEmpty(curr)) return diagnosticsMap.set(collection, diagnostics) void this.checkFloat() if (!this.config.enable || (diagnostics.length > 0 && this._refreshing)) { this._dirties.add(collection) return } let info = await this.getDiagnosticInfo(diagnostics.length === 0) // avoid highlights on invalid state or buffer hidden. if (!info || info.winid == -1) { this._dirties.add(collection) return } let map: Map> = new Map() map.set(collection, diagnostics) this.refresh(map, info) } private async checkFloat(): Promise { if (workspace.bufnr != this.bufnr || !floatFactory) return let pos = await window.getCursorPosition() let diagnostics = this.getDiagnosticsAtPosition(pos) if (diagnostics.length == 0) { floatFactory.close() } } /** * Reset all diagnostics of current buffer */ public async reset(diagnostics: { [collection: string]: Diagnostic[] }): Promise { this.refreshHighlights.clear() let { diagnosticsMap } = this for (let key of diagnosticsMap.keys()) { // make sure clear collection when it's empty. if (isFalsyOrEmpty(diagnostics[key])) diagnostics[key] = [] } for (let [key, value] of Object.entries(diagnostics)) { diagnosticsMap.set(key, value) } this._dirties = new Set(diagnosticsMap.keys()) await this._refresh(false) } public async onCursorHold(lnum: number, col: number): Promise { if (this.config.enableMessage !== 'always') return let pos = this.doc.getPosition(lnum, col) await this.echoMessage(true, pos) } /** * Echo diagnostic message under cursor. */ public async echoMessage(truncate = false, position: Position, target?: string): Promise { const config = this.config if (!config.enable || config.enableMessage === 'never' || this.displayByAle || this.displayByVimDiagnostic) return false if (!target) target = config.messageTarget let useFloat = target == 'float' let diagnostics = this.getDiagnosticsAtPosition(position) if (config.messageLevel) { diagnostics = diagnostics.filter(diagnostic => { return diagnostic.severity && diagnostic.severity <= config.messageLevel }) } if (useFloat) { await this.showFloat(diagnostics) } else { const lines = [] diagnostics.forEach(diagnostic => { lines.push(formatDiagnostic(config.format, diagnostic)) }) if (lines.length) { await this.nvim.command('echo ""') await window.echoLines(lines, truncate) } } return true } public async showVirtualTextCurrentLine(lnum: number): Promise { let { config } = this if (!config.virtualTextCurrentLineOnly || (events.insertMode && !config.refreshOnInsertMode)) return false let enabled = await this.isEnabled() if (!enabled) return false this.showVirtualText(lnum) return true } public async showFloat(diagnostics: DiagnosticWithFileType[], target = 'float'): Promise { if (target !== 'float') return false if (!floatFactory) floatFactory = window.createFloatFactory({ modes: ['n'], autoHide: true }) if (diagnostics.length == 0) { floatFactory.close() return false } if (events.insertMode) return false let config = this.config let ft = '' let docs: Documentation[] = [] if (Object.keys(config.filetypeMap).length > 0) { let filetype = this.doc.filetype const defaultFiletype = config.filetypeMap['default'] || '' ft = config.filetypeMap[filetype] || (defaultFiletype == 'bufferType' ? filetype : defaultFiletype) } diagnostics.forEach(diagnostic => { let filetype = 'Error' if (diagnostic.filetype) { filetype = diagnostic.filetype } else if (ft === '') { switch (diagnostic.severity) { case DiagnosticSeverity.Hint: filetype = 'Hint' break case DiagnosticSeverity.Warning: filetype = 'Warning' break case DiagnosticSeverity.Information: filetype = 'Info' break } } else { filetype = ft } let msg = diagnostic.message let link = diagnostic.codeDescription?.href ?? '' if (config.showRelatedInformation && diagnostic.relatedInformation?.length) { msg = `${diagnostic.message}\n\nRelated information:\n` for (const info of diagnostic.relatedInformation) { const fsPath = URI.parse(info.location.uri).fsPath const basename = path.basename(fsPath) const line = info.location.range.start.line + 1 const column = info.location.range.start.character + 1 msg = `${msg}\n * ${basename}#${line},${column}: ${info.message}` } msg = msg + "\n\n" } docs.push({ filetype, content: formatDiagnostic(config.format, { ...diagnostic, message: msg }) }) if (link) docs.push({ filetype: 'txt', content: link }) }) await floatFactory.show(docs, this.config.floatConfig) return true } /** * Get buffer info needed for refresh. */ private async getDiagnosticInfo(force?: boolean): Promise { let { refreshOnInsertMode } = this._config let { nvim, bufnr } = this let checkInsert = !refreshOnInsertMode if (force) { checkInsert = false } else { let disabledByInsert = events.insertMode && !refreshOnInsertMode if (disabledByInsert) return undefined } return await nvim.call('coc#util#diagnostic_info', [bufnr, checkInsert]).catch(onUnexpectedError) as DiagnosticInfo | undefined } /** * Refresh changed diagnostics to UI. */ private refresh(diagnosticsMap: Map>, info: DiagnosticInfo): void { let { nvim, displayByAle, displayByVimDiagnostic } = this for (let collection of diagnosticsMap.keys()) { this._dirties.delete(collection) } if (displayByAle) { nvim.pauseNotification() for (let [collection, diagnostics] of diagnosticsMap.entries()) { this.refreshAle(collection, diagnostics) } nvim.resumeNotification(true, true) } else if (displayByVimDiagnostic) { nvim.pauseNotification() this.setDiagnosticInfo(true) nvim.resumeNotification(true, true) } else { let emptyCollections: string[] = [] nvim.pauseNotification() for (let [collection, diagnostics] of diagnosticsMap.entries()) { if (diagnostics.length == 0) emptyCollections.push(collection) this.addSigns(collection, diagnostics) this.updateHighlights(collection, diagnostics) } this.showVirtualText(info.lnum) this.updateLocationList(info.winid, info.locationlist) this.setDiagnosticInfo() nvim.resumeNotification(true, true) // cleanup unnecessary collections emptyCollections.forEach(name => { this.diagnosticsMap.delete(name) }) } this._onDidRefresh.fire(this.diagnostics) } public updateLocationList(winid: number, title: string): void { if (!this._config.locationlistUpdate || winid == -1 || title !== 'Diagnostics of coc') return let items = this.toLocationListItems(this.diagnostics) this.nvim.call('coc#ui#setloclist', [winid, items, 'r', 'Diagnostics of coc'], true) } public toLocationListItems(diagnostics: Diagnostic[]): LocationListItem[] { let { locationlistLevel } = this._config let items: LocationListItem[] = [] let lines = this.doc.textDocument.lines diagnostics.sort(sortDiagnostics) for (let diagnostic of diagnostics) { if (locationlistLevel && diagnostic.severity && diagnostic.severity > locationlistLevel) continue items.push(getLocationListItem(this.bufnr, diagnostic, lines)) } return items } public addSigns(collection: string, diagnostics: ReadonlyArray): void { let { enableSign, signLevel } = this._config if (!enableSign) return let group = signGroup + collection let signs: SignItem[] = [] // this.buffer.unplaceSign({ group }) let signsMap: Map = new Map() for (let diagnostic of diagnostics) { let { range, severity } = diagnostic if (!severity || (signLevel && severity > signLevel)) { continue } let line = range.start.line let exists = signsMap.get(line) || [] if (exists.includes(severity)) { continue } exists.push(severity) signsMap.set(line, exists) let priority = this._config.signPriority + 4 - severity signs.push({ name: getNameFromSeverity(severity), lnum: line + 1, priority }) } this.nvim.call('coc#ui#update_signs', [this.bufnr, group, signs], true) } public setDiagnosticInfo(full = false): void { let lnums = [0, 0, 0, 0] let info = { error: 0, warning: 0, information: 0, hint: 0, lnums } let items: DiagnosticItem[] = [] for (let diagnostics of this.diagnosticsMap.values()) { for (let diagnostic of diagnostics) { let lnum = diagnostic.range.start.line + 1 switch (diagnostic.severity) { case DiagnosticSeverity.Warning: info.warning = info.warning + 1 lnums[1] = lnums[1] ? Math.min(lnums[1], lnum) : lnum break case DiagnosticSeverity.Information: info.information = info.information + 1 lnums[2] = lnums[2] ? Math.min(lnums[2], lnum) : lnum break case DiagnosticSeverity.Hint: info.hint = info.hint + 1 lnums[3] = lnums[3] ? Math.min(lnums[3], lnum) : lnum break default: lnums[0] = lnums[0] ? Math.min(lnums[0], lnum) : lnum info.error = info.error + 1 } if (full) { let { start, end } = diagnostic.range items.push({ file: URI.parse(this.doc.uri).fsPath, lnum: start.line + 1, end_lnum: end.line + 1, col: start.character + 1, end_col: end.character + 1, code: diagnostic.code, source: diagnostic.source, message: diagnostic.message, severity: getSeverityName(diagnostic.severity), level: diagnostic.severity ?? 0, location: Location.create(this.doc.uri, diagnostic.range) }) } } } let buf = this.nvim.createBuffer(this.bufnr) buf.setVar('coc_diagnostic_info', info, true) buf.setVar('coc_diagnostic_map', items, true) this.nvim.call('coc#util#do_autocmd', ['CocDiagnosticChange'], true) } public showVirtualText(lnum: number): void { let { _config: config } = this let { virtualText, virtualTextLevel } = config if (!virtualText || lnum < 0) return let { virtualTextPrefix, virtualTextLimitInOneLine, virtualTextCurrentLineOnly } = this._config let { diagnostics, buffer } = this if (virtualTextCurrentLineOnly) { diagnostics = diagnostics.filter(d => { let { start, end } = d.range return start.line <= lnum - 1 && end.line >= lnum - 1 }) } diagnostics.sort(sortDiagnostics) buffer.clearNamespace(virtualTextSrcId) let map: Map = new Map() let opts: VirtualTextOption = { text_align: config.virtualTextAlign, virt_text_win_col: config.virtualTextWinCol } for (let i = diagnostics.length - 1; i >= 0; i--) { let diagnostic = diagnostics[i] if (virtualTextLevel && diagnostic.severity && diagnostic.severity > virtualTextLevel) { continue } let { line } = diagnostic.range.start let highlight = getNameFromSeverity(diagnostic.severity) + 'VirtualText' let msg = diagnostic.message.split(/\n/) .map((l: string) => l.trim()) .filter((l: string) => l.length > 0) .slice(0, this._config.virtualTextLines) .join(this._config.virtualTextLineSeparator) let arr = map.get(line) ?? [] const formattedDiagnostic = formatDiagnostic(this._config.virtualTextFormat, { ...diagnostic, message: msg }) arr.unshift([virtualTextPrefix + stripAnsiColoring(formattedDiagnostic), highlight]) map.set(line, arr) } for (let [line, blocks] of map.entries()) { buffer.setVirtualText(virtualTextSrcId, line, blocks.slice(0, virtualTextLimitInOneLine), opts) } } public updateHighlights(collection: string, diagnostics: ReadonlyArray): void { if (!diagnostics.length) { this.clearHighlight(collection) } else { let items = this.getHighlightItems(diagnostics) let priority = this._config.highlightPriority this.buffer.updateHighlights(NAMESPACE + collection, items, { priority }) } } /** * Refresh all diagnostics */ private async _refresh(dirtyOnly: boolean): Promise { let info = await this.getDiagnosticInfo(!dirtyOnly) if (!info || info.winid == -1 || !this.config.enable) return let { _dirties } = this if (dirtyOnly) { let map: Map> = new Map() for (let [key, diagnostics] of this.diagnosticsMap.entries()) { if (!_dirties.has(key)) continue map.set(key, diagnostics) } this.refresh(map, info) } else { this.refresh(this.diagnosticsMap, info) } } public getHighlightItems(diagnostics: ReadonlyArray): HighlightItem[] { let res: HighlightItem[] = [] for (let i = 0; i < Math.min(this._config.highlightLimit, diagnostics.length); i++) { let diagnostic = diagnostics[i] let hlGroups = getHighlightGroup(diagnostic) for (const hlGroup of hlGroups) { this.doc.addHighlights(res, hlGroup, diagnostic.range) } } // needed for iteration performance and since diagnostic highlight may cross lines. res.sort((a, b) => { if (a.lnum != b.lnum) return a.lnum - b.lnum if (a.colStart != b.colStart) return a.colStart - b.colStart return hlGroups.indexOf(b.hlGroup) - hlGroups.indexOf(a.hlGroup) }) return res } /** * Clear all diagnostics from UI. */ public clear(): void { let { nvim } = this let collections = Array.from(this.diagnosticsMap.keys()) this.refreshHighlights.clear() this._dirties.clear() if (this.displayByAle) { for (let collection of collections) { this.nvim.call(aleMethod, [this.bufnr, collection, []], true) } } else { nvim.pauseNotification() this.buffer.deleteVar('coc_diagnostic_info') this.buffer.deleteVar('coc_diagnostic_map') for (let collection of collections) { this.clearHighlight(collection) this.clearSigns(collection) } if (this._config.virtualText) { this.buffer.clearNamespace(virtualTextSrcId) } nvim.resumeNotification(true, true) } } /** * Get diagnostics at cursor position. */ public getDiagnosticsAt(pos: Position, checkCurrentLine: boolean): Diagnostic[] { let diagnostics: Diagnostic[] = [] for (let diags of this.diagnosticsMap.values()) { if (checkCurrentLine) { diagnostics.push(...diags.filter(o => lineInRange(pos.line, o.range))) } else { diagnostics.push(...diags.filter(o => positionInRange(pos, o.range) == 0)) } } diagnostics.sort(sortDiagnostics) return diagnostics } public getDiagnosticsAtPosition(pos: Position): Diagnostic[] { let { config, doc } = this let res = this.getDiagnosticsAt(pos, config.checkCurrentLine) if (config.checkCurrentLine || res.length) return res // check next character when cursor at end of line. let total = doc.getline(pos.line).length if (pos.character + 1 == total) { res = this.getDiagnosticsAt(Position.create(pos.line, pos.character + 1), false) if (res.length) return res } // check next line when cursor at the beginning of last line. if (pos.line === doc.lineCount - 1 && pos.character == 0) { pos = Position.create(pos.line + 1, 0) res = this.getDiagnosticsAt(pos, true) } return res } public async isEnabled(): Promise { if (this._disposed || !this.config.enable) return false let buf = this.nvim.createBuffer(this.bufnr) let res = await buf.getVar('coc_diagnostic_disable') return res != 1 } public dispose(): void { this.clear() this.diagnosticsMap.clear() this._onDidRefresh.dispose() this._disposed = true } } ================================================ FILE: src/diagnostic/collection.ts ================================================ 'use strict' import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { intersect } from '../util/array' import { Emitter, Event } from '../util/protocol' import workspace from '../workspace' const HintTags = [DiagnosticTag.Deprecated, DiagnosticTag.Unnecessary] export default class DiagnosticCollection { private diagnosticsMap: Map = new Map() private _onDidDiagnosticsChange = new Emitter() public readonly onDidDiagnosticsChange: Event = this._onDidDiagnosticsChange.event constructor( public readonly name: string, private onDispose?: () => void) { } public set(uri: string, diagnostics: Diagnostic[] | undefined): void public set(entries: [string, Diagnostic[] | undefined][]): void public set(entries: [string, Diagnostic[] | undefined][] | string, diagnostics?: Diagnostic[]): void { let diagnosticsPerFile: Map = new Map() if (!Array.isArray(entries)) { let doc = workspace.getDocument(entries) let uri = doc ? doc.uri : entries diagnosticsPerFile.set(uri, diagnostics || []) } else { for (let item of entries) { let [uri, diagnostics] = item let doc = workspace.getDocument(uri) uri = doc ? doc.uri : uri if (diagnostics == null) { // clear previous diagnostics if entry contains null diagnostics = [] } else { diagnostics = (diagnosticsPerFile.get(uri) || []).concat(diagnostics) } diagnosticsPerFile.set(uri, diagnostics) } } for (let item of diagnosticsPerFile) { let [uri, diagnostics] = item uri = URI.parse(uri).toString() diagnostics.forEach(o => { // should be message for the file, but we need range o.range = o.range ?? Range.create(0, 0, 0, 0) o.message = o.message ?? '' o.source = o.source || this.name if (!o.severity && Array.isArray(o.tags) && intersect(o.tags, HintTags)) { o.severity = DiagnosticSeverity.Hint } }) this.diagnosticsMap.set(uri, diagnostics) this._onDidDiagnosticsChange.fire(uri) } } public delete(uri: string): void { this.diagnosticsMap.delete(uri) this._onDidDiagnosticsChange.fire(uri) } public clear(): void { let uris = Array.from(this.diagnosticsMap.keys()) uris = uris.filter(uri => this.diagnosticsMap.get(uri).length > 0) this.diagnosticsMap.clear() for (let uri of uris) { this._onDidDiagnosticsChange.fire(uri) } } public forEach(callback: (uri: string, diagnostics: Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void { for (let uri of this.diagnosticsMap.keys()) { let diagnostics = this.diagnosticsMap.get(uri) callback.call(thisArg, uri, diagnostics, this) } } public entries(): IterableIterator<[string, Diagnostic[]]> { return this.diagnosticsMap.entries() } public get(uri: string): Diagnostic[] { let arr = this.diagnosticsMap.get(uri) return arr == null ? [] : arr.slice() } public has(uri: string): boolean { return this.diagnosticsMap.has(uri) } public dispose(): void { this.clear() if (this.onDispose) this.onDispose() this._onDidDiagnosticsChange.dispose() } } ================================================ FILE: src/diagnostic/manager.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Location, Position, Range, TextDocumentIdentifier } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import events from '../events' import BufferSync from '../model/bufferSync' import { disposeAll } from '../util' import { isFalsyOrEmpty } from '../util/array' import { isVim } from '../util/constants' import { readFileLines } from '../util/fs' import { comparePosition, rangeIntersect } from '../util/position' import { Disposable, Emitter, Event } from '../util/protocol' import { byteIndex } from '../util/string' import window from '../window' import workspace from '../workspace' import { DiagnosticBuffer } from './buffer' import DiagnosticCollection from './collection' import { getSeverityName, severityLevel } from './util' export interface DiagnosticEventParams { bufnr: number uri: string diagnostics: ReadonlyArray } interface DiagnosticSignConfig { messageDelay?: number errorSign?: string warningSign?: string infoSing?: string hintSign?: string enableHighlightLineNumber?: boolean } export interface DiagnosticItem { file: string bufnr?: number lnum: number end_lnum: number col: number end_col: number source: string code: string | number message: string severity: string level: number location: Location } interface PrepareResult { item: DiagnosticBuffer wrapscan: boolean ranges: ReadonlyArray curpos: Position } class DiagnosticManager implements Disposable { private readonly _onDidRefresh = new Emitter() public readonly onDidRefresh: Event = this._onDidRefresh.event private enabled = true private buffers: BufferSync | undefined private collections: DiagnosticCollection[] = [] private disposables: Disposable[] = [] private messageTimer: NodeJS.Timeout public init(): void { commands.register({ id: 'workspace.diagnosticRelated', execute: () => this.jumpRelated() }, false, 'jump to related locations of current diagnostic.') this.defineSigns(workspace.initialConfiguration.get('diagnostic')) let globalValue = workspace.initialConfiguration.inspect('diagnostic.enable').globalValue this.enabled = globalValue !== false this.buffers = workspace.registerBufferSync(doc => { let buf = new DiagnosticBuffer(this.nvim, doc) buf.onDidRefresh(diagnostics => { this._onDidRefresh.fire({ diagnostics, uri: buf.uri, bufnr: buf.bufnr }) }) let diagnostics = this.getDiagnostics(buf) // ignore empty diagnostics on first time. if (Object.keys(diagnostics).length > 0 && buf.config.autoRefresh) { void buf.reset(diagnostics) } return buf }) workspace.onDidChangeConfiguration(e => { if (this.buffers && e.affectsConfiguration('diagnostic')) { for (let item of this.buffers.items) { item.loadConfiguration() } } }, null, this.disposables) let config = workspace.initialConfiguration.get('diagnostic') events.on('CursorMoved', (bufnr, cursor) => { if (this.messageTimer) clearTimeout(this.messageTimer) this.messageTimer = setTimeout(() => { let buf = this.buffers.getItem(bufnr) if (buf == null || buf.dirty) return void Promise.allSettled([ buf.onCursorHold(cursor[0], cursor[1]), buf.showVirtualTextCurrentLine(cursor[0])]) }, config.messageDelay) }, null, this.disposables) events.on(['InsertEnter', 'BufEnter'], () => { clearTimeout(this.messageTimer) }, null, this.disposables) events.on('InsertLeave', bufnr => { let buf = this.buffers.getItem(bufnr) if (!buf || buf.config.refreshOnInsertMode) return for (let buf of this.buffers.items) { buf.refreshHighlights() } }, null, this.disposables) events.on('BufWinEnter', (bufnr: number) => { let buf = this.buffers.getItem(bufnr) if (buf) buf.refreshHighlights() }, null, this.disposables) this.checkConfigurationErrors() workspace.configurations.onError(ev => { const collection = this.create('config') collection.set(ev.uri, ev.diagnostics) }, null, this.disposables) } public checkConfigurationErrors(): void { const errors = workspace.configurations.errors if (!isFalsyOrEmpty(errors)) { const collection = this.create('config') for (let [uri, diagnostics] of errors.entries()) { let fsPath = URI.parse(uri).fsPath void window.showErrorMessage(`Error detected for config file ${fsPath}, please check diagnostics list.`) collection.set(uri, diagnostics) } } } public defineSigns(config: DiagnosticSignConfig): void { let { nvim } = this nvim.pauseNotification() for (let kind of ['Error', 'Warning', 'Info', 'Hint']) { let cmd = `sign define Coc${kind} linehl=Coc${kind}Line` let signText = config[kind.toLowerCase() + 'Sign'] if (signText) cmd += ` texthl=Coc${kind}Sign text=${signText}` if (!isVim && config.enableHighlightLineNumber) cmd += ` numhl=Coc${kind}Sign` nvim.command(cmd, true) } nvim.resumeNotification(false, true) } public getItem(bufnr: number): DiagnosticBuffer | undefined { return this.buffers.getItem(bufnr) } /** * Fill location list with diagnostics */ public async setLocationlist(bufnr: number): Promise { let doc = workspace.getAttachedDocument(bufnr) let buf = this.getItem(doc.bufnr) let diagnostics: Diagnostic[] = [] for (let diags of Object.values(this.getDiagnostics(buf))) { diagnostics.push(...diags) } let items = buf.toLocationListItems(diagnostics) await this.nvim.call('coc#ui#setloclist', [0, items, ' ', 'Diagnostics of coc']) } /** * Create collection by name */ public create(name: string): DiagnosticCollection { let collection = this.getCollectionByName(name) if (collection) return collection collection = new DiagnosticCollection(name, () => { let idx = this.collections.findIndex(o => o == collection) if (idx !== -1) this.collections.splice(idx, 1) }) this.collections.push(collection) collection.onDidDiagnosticsChange(uri => { let buf = this.buffers?.getItem(uri) if (buf && buf.config.autoRefresh) void buf.update(name, this.getDiagnosticsByCollection(buf, collection)) }) return collection } /** * Get diagnostics ranges from document */ public getSortedRanges(uri: string, minLevel: number | undefined, severity?: string): Range[] { let collections = this.getCollections(uri) let res: Range[] = [] let level = severity ? severityLevel(severity) : 0 for (let collection of collections) { let diagnostics = collection.get(uri) if (level) { diagnostics = diagnostics.filter(o => o.severity == level) } else { if (minLevel && minLevel < DiagnosticSeverity.Hint) { diagnostics = diagnostics.filter(o => { return o.severity && o.severity > minLevel ? false : true }) } } let ranges = diagnostics.map(o => o.range) res.push(...ranges) } res.sort((a, b) => { if (a.start.line != b.start.line) { return a.start.line - b.start.line } return a.start.character - b.start.character }) return res } /** * Get readonly diagnostics for a buffer */ public getDiagnostics(buf: DiagnosticBuffer): { [collection: string]: Diagnostic[] } { let res: { [collection: string]: Diagnostic[] } = {} for (let collection of this.collections) { if (!collection.has(buf.uri)) continue res[collection.name] = this.getDiagnosticsByCollection(buf, collection) } return res } /** * Get filtered diagnostics by collection. */ public getDiagnosticsByCollection(buf: DiagnosticBuffer, collection: DiagnosticCollection): Diagnostic[] { // let config = this.buffers.getItem(uri) let { level, showUnused, showDeprecated } = buf.config let items = collection.get(buf.uri) ?? [] if (items.length) { items = items.filter(d => { if (level && d.severity && d.severity > level) { return false } if (!showUnused && d.tags?.includes(DiagnosticTag.Unnecessary)) { return false } if (!showDeprecated && d.tags?.includes(DiagnosticTag.Deprecated)) { return false } return true }) items.sort((a, b) => { return comparePosition(a.range.start, b.range.start) }) } return items } public getDiagnosticsInRange(document: TextDocumentIdentifier, range: Range): Diagnostic[] { let res: Diagnostic[] = [] for (let collection of this.collections) { for (let item of collection.get(document.uri) ?? []) { if (rangeIntersect(item.range, range)) { res.push(item) } } } return res } /** * Show diagnostics under cursor in preview window */ public async preview(): Promise { let diagnostics = await this.getCurrentDiagnostics() if (diagnostics.length == 0) { this.nvim.command('pclose', true) return } let lines: string[] = [] for (let diagnostic of diagnostics) { let { source, code, severity, message } = diagnostic let s = getSeverityName(severity)[0] lines.push(`[${source}${code ? ' ' + code : ''}] [${s}]`) lines.push(...message.split(/\r?\n/)) lines.push('') } this.nvim.call('coc#ui#preview_info', [lines, 'txt'], true) } private async prepareJump(severity?: string): Promise { let bufnr = await this.nvim.call('bufnr', ['%']) as number let item = this.buffers.getItem(bufnr) if (!item) return let ranges = this.getSortedRanges(item.uri, item.config.level, severity) if (isFalsyOrEmpty(ranges)) return let curpos = await window.getCursorPosition() let wrapscan = await this.nvim.getOption('wrapscan') return { item, curpos, wrapscan: wrapscan != 0, ranges } } /** * Jump to previous diagnostic position */ public async jumpPrevious(severity?: string): Promise { let result = await this.prepareJump(severity) if (!result) return let { curpos, item, wrapscan, ranges } = result let pos: Position for (let i = ranges.length - 1; i >= 0; i--) { let end = ranges[i].end if (comparePosition(end, curpos) < 0) { pos = ranges[i].start break } } if (!pos && wrapscan) pos = ranges[ranges.length - 1].start if (pos) { await window.moveTo(pos) await item.echoMessage(false, pos) } else { void window.showWarningMessage(`No more diagnostic before cursor position`) } } /** * Jump to next diagnostic position */ public async jumpNext(severity?: string): Promise { let result = await this.prepareJump(severity) if (!result) return let { curpos, item, wrapscan, ranges } = result let pos: Position for (let i = 0; i <= ranges.length - 1; i++) { let start = ranges[i].start if (comparePosition(start, curpos) > 0) { // The position could be invalid (ex: exceed end of line) let arr = await this.nvim.call('coc#util#valid_position', [start.line, start.character]) if ((arr[0] != start.line || arr[1] != start.character) && comparePosition(Position.create(arr[0], arr[1]), curpos) <= 0) { continue } pos = Position.create(arr[0], arr[1]) break } } if (!pos && wrapscan) pos = ranges[0].start if (pos) { await window.moveTo(pos) await item.echoMessage(false, pos) } else { void window.showWarningMessage(`No more diagnostic after cursor position`) } } /** * Get all sorted diagnostics */ public async getDiagnosticList(): Promise { let res: DiagnosticItem[] = [] let config = workspace.getConfiguration('diagnostic') let level = severityLevel(config.get('level', 'hint')) for (let collection of this.collections) { for (let [uri, diagnostics] of collection.entries()) { if (diagnostics.length == 0) continue let u = URI.parse(uri) let doc = workspace.getDocument(uri) let lines = doc && doc.attached ? doc.textDocument.lines : undefined if (!lines && u.scheme === 'file') { try { const max = diagnostics.reduce((p, c) => { return Math.max(c.range.end.line, p) }, 0) lines = await readFileLines(u.fsPath, 0, max) } catch (e) {} } for (let diagnostic of diagnostics) { if (diagnostic.severity && diagnostic.severity > level) continue let { start, end } = diagnostic.range let o: DiagnosticItem = { file: u.fsPath, bufnr: doc ? doc.bufnr : undefined, lnum: start.line + 1, end_lnum: end.line + 1, col: Array.isArray(lines) ? byteIndex(lines[start.line] ?? '', start.character) + 1 : start.character + 1, end_col: Array.isArray(lines) ? byteIndex(lines[end.line] ?? '', end.character) + 1 : end.character + 1, code: diagnostic.code, source: diagnostic.source ?? collection.name, message: diagnostic.message, severity: getSeverityName(diagnostic.severity), level: diagnostic.severity ?? 0, location: Location.create(uri, diagnostic.range) } res.push(o) } } } res.sort((a, b) => { if (a.level !== b.level) { return a.level - b.level } if (a.file !== b.file) { return a.file > b.file ? 1 : -1 } else { if (a.lnum != b.lnum) { return a.lnum - b.lnum } return a.col - b.col } }) return res } private async getBufferAndPosition(): Promise<[DiagnosticBuffer, Position] | undefined> { let [bufnr, lnum, col] = await this.nvim.eval(`[bufnr("%"),line('.'),col('.')]`) as [number, number, number] let item = this.buffers.getItem(bufnr) if (!item) return let pos = item.doc.getPosition(lnum, col) return [item, pos] } public async getCurrentDiagnostics(): Promise { let res = await this.getBufferAndPosition() if (!res) return return res[0].getDiagnosticsAtPosition(res[1]) } public async echoCurrentMessage(target?: string): Promise { let res = await this.getBufferAndPosition() if (!res) return let [item, position] = res await item.echoMessage(false, position, target) } public async jumpRelated(): Promise { let locations = await this.relatedInformation() if (locations.length == 1) { await workspace.jumpTo(locations[0].uri, locations[0].range.start) } else if (locations.length > 1) { await workspace.showLocations(locations) } else { void window.showWarningMessage('No related information found.') } } public async relatedInformation(): Promise { let diagnostics = await this.getCurrentDiagnostics() let diagnostic = diagnostics.find(o => o.relatedInformation != null) let locations = diagnostic ? diagnostic.relatedInformation.map(o => o.location) : [] return locations } public reset(): void { clearTimeout(this.messageTimer) this.buffers.reset() for (let collection of this.collections) { collection.dispose() } this.collections = [] } public dispose(): void { clearTimeout(this.messageTimer) this.buffers.dispose() for (let collection of this.collections) { collection.dispose() } this.collections = [] disposeAll(this.disposables) } private get nvim(): Neovim { return workspace.nvim } public getCollectionByName(name: string): DiagnosticCollection { return this.collections.find(o => o.name == name) } private getCollections(uri: string): DiagnosticCollection[] { return this.collections.filter(c => c.has(uri)) } public async toggleDiagnostic(enable?: number): Promise { this.enabled = enable == undefined ? !this.enabled : enable != 0 await Promise.allSettled(this.buffers.items.map(buf => { return buf.setState(this.enabled) })) } public async toggleDiagnosticBuffer(bufnr?: number, enable?: number): Promise { bufnr = bufnr ?? workspace.bufnr let buf = this.buffers.getItem(bufnr) if (buf) { let isEnabled = enable == undefined ? await buf.isEnabled() : enable == 0 await this.nvim.call('setbufvar', [bufnr, 'coc_diagnostic_disable', isEnabled ? 1 : 0]) await buf.setState(!isEnabled) } } /** * Refresh diagnostics by uri or bufnr */ public async refreshBuffer(uri: string | number): Promise { let buf = this.buffers.getItem(uri) if (!buf) return false await buf.reset(this.getDiagnostics(buf)) return true } /** * Force diagnostics refresh. */ public async refresh(bufnr?: number): Promise { let items: Iterable if (!bufnr) { items = this.buffers.items } else { let item = this.buffers.getItem(bufnr) items = item ? [item] : [] } for (let item of items) { await this.refreshBuffer(item.uri) } } } export default new DiagnosticManager() ================================================ FILE: src/diagnostic/util.ts ================================================ 'use strict' import type { VirtualTextOption } from '@chemzqm/neovim' import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Range, TextEdit } from 'vscode-languageserver-types' import { FloatConfig } from '../types' import { comparePosition, rangeOverlap } from '../util/position' import { byteIndex } from '../util/string' import { getPosition } from '../util/textedit' export interface LocationListItem { bufnr: number lnum: number end_lnum: number col: number end_col: number text: string type: string } export enum DiagnosticHighlight { Error = 'CocErrorHighlight', Warning = 'CocWarningHighlight', Information = 'CocInfoHighlight', Hint = 'CocHintHighlight', Deprecated = 'CocDeprecatedHighlight', Unused = 'CocUnusedHighlight' } /** * Local diagnostic config */ export interface DiagnosticConfig { enable: boolean highlightLimit: number highlightPriority: number autoRefresh: boolean enableSign: boolean locationlistUpdate: boolean enableHighlightLineNumber: boolean checkCurrentLine: boolean enableMessage: string displayByAle: boolean displayByVimDiagnostic: boolean signPriority: number level: number locationlistLevel: number | undefined signLevel: number | undefined messageLevel: number | undefined messageTarget: string refreshOnInsertMode: boolean virtualText: boolean virtualTextAlign: VirtualTextOption['text_align'] virtualTextLevel: number | undefined virtualTextWinCol: number | null virtualTextCurrentLineOnly: boolean virtualTextPrefix: string virtualTextFormat: string virtualTextLimitInOneLine: number virtualTextLines: number virtualTextLineSeparator: string filetypeMap: object showUnused: boolean showDeprecated: boolean format: string floatConfig: FloatConfig showRelatedInformation: boolean } export function formatDiagnostic(format: string, diagnostic: Diagnostic): string { let { source, code, severity, message } = diagnostic let s = getSeverityName(severity)[0] const codeStr = code ? ' ' + code : '' return format.replace('%source', source) .replace('%code', codeStr) .replace('%severity', s) .replace('%message', message) } export function getSeverityName(severity: DiagnosticSeverity): string { switch (severity) { case DiagnosticSeverity.Warning: return 'Warning' case DiagnosticSeverity.Information: return 'Information' case DiagnosticSeverity.Hint: return 'Hint' default: return 'Error' } } export function getSeverityType(severity: DiagnosticSeverity): string { switch (severity) { case DiagnosticSeverity.Warning: return 'W' case DiagnosticSeverity.Information: return 'I' case DiagnosticSeverity.Hint: return 'I' default: return 'E' } } export function severityLevel(level: string | null | undefined): number | undefined { if (level == null) return undefined switch (level) { case 'hint': return DiagnosticSeverity.Hint case 'information': return DiagnosticSeverity.Information case 'warning': return DiagnosticSeverity.Warning case 'error': return DiagnosticSeverity.Error default: return DiagnosticSeverity.Hint } } export function getNameFromSeverity(severity: DiagnosticSeverity): string { switch (severity) { case DiagnosticSeverity.Error: return 'CocError' case DiagnosticSeverity.Warning: return 'CocWarning' case DiagnosticSeverity.Information: return 'CocInfo' case DiagnosticSeverity.Hint: return 'CocHint' default: return 'CocError' } } export function getLocationListItem(bufnr: number, diagnostic: Diagnostic, lines?: ReadonlyArray): LocationListItem { let { start, end } = diagnostic.range let owner = diagnostic.source || 'coc.nvim' let msg = diagnostic.message.split('\n')[0] let type = getSeverityName(diagnostic.severity).slice(0, 1).toUpperCase() return { bufnr, lnum: start.line + 1, end_lnum: end.line + 1, col: Array.isArray(lines) ? byteIndex(lines[start.line] ?? '', start.character) + 1 : start.character + 1, end_col: Array.isArray(lines) ? byteIndex(lines[end.line] ?? '', end.character) + 1 : end.character + 1, text: `[${owner}${diagnostic.code ? ' ' + diagnostic.code : ''}] ${msg} [${type}]`, type } } /** * Sort by severity and position */ export function sortDiagnostics(a: Diagnostic, b: Diagnostic): number { let sa = a.severity ?? 1 let sb = b.severity ?? 1 if (sa != sb) return sa - sb let d = comparePosition(a.range.start, b.range.start) if (d != 0) return d return a.source > b.source ? 1 : -1 } export function getHighlightGroup(diagnostic: Diagnostic): DiagnosticHighlight[] { let hlGroups: DiagnosticHighlight[] = [] let tags = diagnostic.tags || [] if (tags.includes(DiagnosticTag.Deprecated)) { hlGroups.push(DiagnosticHighlight.Deprecated) } if (tags.includes(DiagnosticTag.Unnecessary)) { hlGroups.push(DiagnosticHighlight.Unused) } switch (diagnostic.severity) { case DiagnosticSeverity.Hint: hlGroups.push(DiagnosticHighlight.Hint) break case DiagnosticSeverity.Information: hlGroups.push(DiagnosticHighlight.Information) break case DiagnosticSeverity.Warning: hlGroups.push(DiagnosticHighlight.Warning) break case DiagnosticSeverity.Error: hlGroups.push(DiagnosticHighlight.Error) break } return hlGroups } export function adjustDiagnostics(diagnostics: ReadonlyArray, edit: TextEdit): ReadonlyArray { let res: Diagnostic[] = [] let { range } = edit for (let diag of diagnostics) { let r = diag.range if (rangeOverlap(range, r)) continue if (comparePosition(r.start, range.end) > 0) { let s = getPosition(r.start, edit) let e = getPosition(r.end, edit) if (s.line >= 0 && s.character >= 0 && e.line >= 0 && e.character >= 0) { diag.range = Range.create(s, e) } } res.push(diag) } return res } ================================================ FILE: src/events.ts ================================================ 'use strict' import type { CompleteDoneItem, CompleteOption } from './completion/types' import { createLogger } from './logger' import { JumpInfo } from './types' import { disposeAll, getConditionValue } from './util' import { onUnexpectedError, shouldIgnore } from './util/errors' import * as Is from './util/is' import { equals } from './util/object' import { CancellationToken, Disposable } from './util/protocol' import { byteSlice } from './util/string' const logger = createLogger('events') const debounceTime = getConditionValue(100, 10) export type Result = void | Promise export interface PopupChangeEvent { readonly startcol: number readonly index: number readonly word: string readonly height: number readonly width: number readonly row: number readonly col: number readonly size: number readonly scrollbar: boolean readonly inserted: boolean readonly move: boolean } export interface VisibleEvent { winid: number bufnr: number /** * 1 based, end inclusive topline, botline */ region: [number, number] } export interface ModeChangedEvent { readonly old_mode: string readonly new_mode: string } export interface InsertChange { readonly lnum: number readonly col: number readonly line: string readonly changedtick: number pre: string /** * Insert character that cause change of this time. */ insertChar?: string insertChars?: string[] } export enum EventName { Ready = 'ready', InsertEnter = 'InsertEnter', InsertLeave = 'InsertLeave', CursorHoldI = 'CursorHoldI', CursorMovedI = 'CursorMovedI', CursorHold = 'CursorHold', CursorMoved = 'CursorMoved', MenuPopupChanged = 'MenuPopupChanged', InsertCharPre = 'InsertCharPre', TextChanged = 'TextChanged', BufEnter = 'BufEnter', TextChangedI = 'TextChangedI', TextChangedP = 'TextChangedP', TextInsert = 'TextInsert', BufWinEnter = 'BufWinEnter', WinScrolled = 'WinScrolled', WinClosed = 'WinClosed', BufWinLeave = 'BufWinLeave', ModeChanged = 'ModeChanged' } export type BufEvents = 'BufHidden' | 'BufEnter' | 'BufRename' | 'InsertLeave' | 'TermOpen' | 'InsertEnter' | 'BufCreate' | 'BufUnload' | 'BufDetach' | 'Enter' | 'LinesChanged' | 'PumNavigate' export type EmptyEvents = 'FocusGained' | 'ColorScheme' | 'FocusLost' | 'InsertSnippet' | 'VimLeavePre' | 'ready' export type InsertChangeEvents = 'TextChangedP' | 'TextChangedI' export type TaskEvents = 'TaskExit' | 'TaskStderr' | 'TaskStdout' export type WindowEvents = 'WinLeave' | 'WinEnter' | 'WinClosed' export type TabEvents = 'TabNew' | 'TabClosed' export type AllEvents = BufEvents | EmptyEvents | CursorEvents | TaskEvents | WindowEvents | TabEvents | InsertChangeEvents | 'CompleteDone' | 'TextChanged' | 'MenuPopupChanged' | 'BufWritePost' | 'BufWritePre' | 'ModeChanged' | 'InsertCharPre' | 'FileType' | 'BufWinEnter' | 'BufWinLeave' | 'VimResized' | 'TermExit' | 'WinScrolled' | 'CompleteStart' | 'DirChanged' | 'OptionSet' | 'Command' | 'BufReadCmd' | 'GlobalChange' | 'InputChar' | 'PlaceholderJump' | 'InputListSelect' | 'WinLeave' | 'MenuInput' | 'PromptInsert' | 'PromptExit' | 'FloatBtnClick' | 'InsertSnippet' | 'TextInsert' | 'PromptKeyPress' | 'WindowVisible' export type CursorEvents = CursorHoldEvents | CursorMoveEvents export type CursorHoldEvents = 'CursorHold' | 'CursorHoldI' export type CursorMoveEvents = 'CursorMoved' | 'CursorMovedI' export type OptionValue = string | number | boolean export interface CursorPosition { readonly bufnr: number readonly lnum: number readonly col: number readonly insert: boolean } export interface LatestInsert { readonly bufnr: number readonly character: string readonly timestamp: number } class Events { private handlers: Map Promise)[]> = new Map() private _cursor: CursorPosition private _bufnr = 1 private timeoutMap: Map = new Map() // bufnr & character private _recentInserts: [number, string][] = [] private _lastChange = 0 private _insertMode = false private _pumAlignTop = false private _pumVisible = false private _completing = false private _requesting = false private _ready = false private _mode = 'n' private _pumInserted = false public timeout = 1000 // public completing = false public set requesting(val: boolean) { this._requesting = val } public get requesting(): boolean { return this._requesting } public get ready(): boolean { return this._ready } public get mode(): string { return this._mode } public get pumInserted(): boolean { return this._pumInserted } private fireVisibleEvent(ev: VisibleEvent): void { let { winid } = ev let timer = this.timeoutMap.get(winid) if (timer) clearTimeout(timer) timer = setTimeout(() => { this.fire('WindowVisible', [ev]).catch(onUnexpectedError) }, debounceTime) this.timeoutMap.set(winid, timer) } private clearVisibleTimer(winid: number): void { let timer = this.timeoutMap.get(winid) if (timer) { this.timeoutMap.delete(winid) clearTimeout(timer) } } public set completing(completing: boolean) { this._completing = completing this._pumVisible = completing this._pumInserted = false } public get completing(): boolean { return this._completing } public get cursor(): CursorPosition { return this._cursor ?? { bufnr: this._bufnr, col: 1, lnum: 1, insert: false } } public get bufnr(): number { return this._bufnr } public get pumvisible(): boolean { return this._pumVisible } public get pumAlignTop(): boolean { return this._pumAlignTop } public get insertMode(): boolean { return this._insertMode && this._mode.startsWith('i') } public get lastChangeTs(): number { return this._lastChange } /** * Resolved when first event fired or timeout */ public race(events: AllEvents[], token?: number | CancellationToken): Promise<{ name: AllEvents, args: unknown[] } | undefined> { let disposables: Disposable[] = [] return new Promise(resolve => { if (Is.number(token)) { let timer = setTimeout(() => { disposeAll(disposables) resolve(undefined) }, token) disposables.push(Disposable.create(() => { clearTimeout(timer) })) } else if (CancellationToken.is(token)) { token.onCancellationRequested(() => { disposeAll(disposables) resolve(undefined) }, null, disposables) } events.forEach(ev => { this.on(ev, (...args) => { disposeAll(disposables) resolve({ name: ev, args }) }, null, disposables) }) }) } public async fire(event: string, args: any[]): Promise { if (event === EventName.Ready) { this._ready = true } else if (event == EventName.InsertEnter) { this._insertMode = true } else if (event == EventName.InsertLeave) { this._insertMode = false this._pumVisible = false this._recentInserts = [] } else if (event == EventName.CursorHoldI || event == EventName.CursorMovedI) { this._bufnr = args[0] if (!this._insertMode) { this._insertMode = true void this.fire(EventName.InsertEnter, [args[0]]) } } else if (event == EventName.CursorHold || event == EventName.CursorMoved) { this._bufnr = args[0] if (this._insertMode) { this._insertMode = false void this.fire(EventName.InsertLeave, [args[0]]) } } else if (event == EventName.MenuPopupChanged) { this._pumVisible = true this._pumAlignTop = args[1] > args[0].row this._pumInserted = args[0].inserted } else if (event == EventName.InsertCharPre) { this._recentInserts.push([args[1], args[0]]) } else if (event == EventName.TextChanged) { this._lastChange = Date.now() } else if (event == EventName.BufEnter) { this._bufnr = args[0] } else if (event == EventName.TextChangedI || event == EventName.TextChangedP) { let info: InsertChange = args[1] let pre = byteSlice(info.line ?? '', 0, info.col - 1) let arr: [number, string][] arr = this._recentInserts.filter(o => o[0] == args[0]) this._bufnr = args[0] this._recentInserts = [] this._lastChange = Date.now() info.pre = pre info.insertChars = arr.map(o => o[1]) // fix cursor since vim may not send CursorMovedI event this._cursor = Object.freeze({ bufnr: args[0], lnum: info.lnum, col: info.col, insert: true }) if (arr.length && pre.length) { let character = pre.slice(-1) if (arr.findIndex(o => o[1] == character) !== -1) { info.insertChar = character // make it fires after TextChangedI & TextChangedP process.nextTick(() => { void this.fire(EventName.TextInsert, [...args, character]) }) } } } else if (event == EventName.BufWinEnter) { const [bufnr, winid, region] = args this.fireVisibleEvent({ bufnr, winid, region }) } else if (event == EventName.WinScrolled) { const [winid, bufnr, region] = args this.fireVisibleEvent({ bufnr, winid, region }) } else if (event == EventName.WinClosed) { this.clearVisibleTimer(args[0]) } else if (event == EventName.BufWinLeave) { this.clearVisibleTimer(args[1]) } else if (event == EventName.ModeChanged) { this._mode = args[0].new_mode } if (event == EventName.CursorMoved || event == EventName.CursorMovedI) { args.push(this._recentInserts.length > 0) let cursor = { bufnr: args[0], lnum: args[1][0], col: args[1][1], insert: event == EventName.CursorMovedI } // Avoid CursorMoved event when it's not moved at all if ((this._cursor && equals(this._cursor, cursor))) return this._cursor = cursor } let cbs = this.handlers.get(event) if (cbs?.length) { let fns = cbs.slice() let traceSlow = this.requesting await Promise.allSettled(fns.map(fn => { let promiseFn = async () => { let timer: NodeJS.Timeout if (traceSlow) { timer = setTimeout(() => { logger.warn(`Slow "${event}" handler detected`, fn['stack']) }, this.timeout) } try { await fn(args) } catch (e) { if (!shouldIgnore(e)) logger.error(`Error on event: ${event}`, e, fn['stack']) } clearTimeout(timer) } return promiseFn() })) } } public on(event: BufEvents | 'PromptExit', handler: (bufnr: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: CursorHoldEvents, handler: (bufnr: number, cursor: [number, number], winid: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: InsertChangeEvents, handler: (bufnr: number, info: InsertChange) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: WindowEvents, handler: (winid: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: CursorMoveEvents, handler: (bufnr: number, cursor: [number, number], hasInsert: boolean) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'WindowVisible', handler: (event: VisibleEvent) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'WinScrolled', handler: (winid: number, bufnr: number, region: Readonly<[number, number]>) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TabClosed', handler: (tabids: number[]) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TabNew', handler: (tabid: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TextInsert', handler: (bufnr: number, info: InsertChange, character: string) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'FloatBtnClick', handler: (bufnr: number, index: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'PromptKeyPress', handler: (bufnr: number, key: string) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'BufWritePre', handler: (bufnr: number, bufname: string, changedtick: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TextChanged' | 'BufWritePost', handler: (bufnr: number, changedtick: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TaskExit', handler: (id: string, code: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TaskStderr' | 'TaskStdout', handler: (id: string, lines: string[]) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'BufReadCmd', handler: (scheme: string, fullpath: string) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'VimResized', handler: (columns: number, lines: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'Command', handler: (name: string) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'MenuPopupChanged', handler: (event: PopupChangeEvent, cursorline: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'CompleteDone', handler: (item: CompleteDoneItem | object, line: number, bufnr: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'CompleteStart', handler: (option: Readonly) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'InsertCharPre', handler: (character: string, bufnr: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'FileType', handler: (filetype: string, bufnr: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'BufWinEnter' | 'BufWinLeave', handler: (bufnr: number, winid: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'TermExit', handler: (bufnr: number, status: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'DirChanged', handler: (cwd: string) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'OptionSet' | 'GlobalChange', handler: (option: string, oldVal: OptionValue, newVal: OptionValue) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'InputChar', handler: (session: string, character: string, mode: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'PromptInsert', handler: (value: string, bufnr: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'PlaceholderJump', handler: (bufnr: number, info: JumpInfo) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'InputListSelect', handler: (index: number) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: 'ModeChanged', handler: (event: ModeChangedEvent) => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: EmptyEvents, handler: () => Result, thisArg?: any, disposables?: Disposable[]): Disposable public on(event: AllEvents | AllEvents[], handler: (...args: unknown[]) => Result, thisArg?: any, disposables?: Disposable[] | true): Disposable public on(event: AllEvents[] | AllEvents, handler: (...args: any[]) => Result, thisArg?: any, disposables?: Disposable[] | true): Disposable { if (Array.isArray(event)) { let arr: Disposable[] = [] for (let ev of event) { this.on(ev as any, handler, thisArg, arr) } let dis = Disposable.create(() => { disposeAll(arr) }) if (Array.isArray(disposables)) { disposables.push(dis) } return dis } else { let arr = this.handlers.get(event) ?? [] let onFinish = () => { if (disposables === true && disposable) disposable.dispose() } let wrappedhandler = args => new Promise((resolve, reject) => { try { Promise.resolve(handler.apply(thisArg ?? null, args)).then(() => { onFinish() resolve(undefined) }, (e: Error) => { onFinish() reject(e) }) } catch (e) { onFinish() reject(e as Error) } }) Error.captureStackTrace(wrappedhandler) arr.push(wrappedhandler) this.handlers.set(event, arr) let disposable = Disposable.create(() => { let idx = arr.indexOf(wrappedhandler) if (idx !== -1) { arr.splice(idx, 1) } }) if (Array.isArray(disposables)) { disposables.push(disposable) } return disposable } } public once(event: AllEvents, handler: (...args: any[]) => Result, thisArg?: any): Disposable { return this.on(event, handler, thisArg, true) } } export default new Events() ================================================ FILE: src/extension/index.ts ================================================ 'use strict' import { WorkspaceFolder } from 'vscode-languageserver-types' import commands from '../commands' import { ConfigurationUpdateTarget } from '../configuration/types' import events from '../events' import { createLogger } from '../logger' import type { OutputChannel } from '../types' import { concurrent } from '../util' import { distinct, isFalsyOrEmpty, toArray } from '../util/array' import { VERSION, dataHome } from '../util/constants' import { isUrl } from '../util/is' import { fs, path, which } from '../util/node' import { toObject } from '../util/object' import { executable } from '../util/processes' import { Event } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { IInstaller, Installer } from './installer' import { API, Extension, ExtensionInfo, ExtensionItem, ExtensionManager, ExtensionState, ExtensionToLoad } from './manager' import { ExtensionStat, checkExtensionRoot, loadExtensionJson, loadGlobalJsonAsync } from './stat' import { InstallBuffer, InstallChannel, InstallUI } from './ui' const logger = createLogger('extensions-index') export interface PropertyScheme { type: string default: any description: string enum?: string[] items?: any [key: string]: any } export interface UpdateSettings { updateCheck: string updateUIInTab: boolean silentAutoupdate: boolean } const EXTENSIONS_FOLDER = path.join(dataHome, 'extensions') // global local file native export class Extensions { public readonly manager: ExtensionManager public readonly states: ExtensionStat public modulesFolder = path.join(EXTENSIONS_FOLDER, 'node_modules') private globalPromise: Promise constructor() { checkExtensionRoot(EXTENSIONS_FOLDER) this.states = new ExtensionStat(EXTENSIONS_FOLDER) this.manager = new ExtensionManager(this.states, EXTENSIONS_FOLDER) commands.register({ id: 'extensions.forceUpdateAll', execute: async () => { let arr = await this.manager.cleanExtensions() logger.info(`Force update extensions: ${arr}`) await this.installExtensions(arr) } }, false, 'remove all global extensions and install them') this.globalPromise = this.globalExtensions() commands.register({ id: 'extensions.toggleAutoUpdate', execute: async () => { let settings = this.getUpdateSettings() let target = ConfigurationUpdateTarget.Global let config = workspace.getConfiguration(null, null) if (settings.updateCheck == 'never') { await config.update('extensions.updateCheck', 'daily', target) void window.showInformationMessage('Extension auto update enabled.') } else { await config.update('extensions.updateCheck', 'never', target) void window.showInformationMessage('Extension auto update disabled.') } await config.update('coc.preferences.extensionUpdateCheck', undefined, target) } }, false, 'toggle auto update of extensions.') events.once('ready', () => { void this.checkRecommendation(workspace.workspaceFolders[0]) workspace.onDidChangeWorkspaceFolders(e => { void this.checkRecommendation(e.added[0]) }) }) } public async checkRecommendation(workspaceFolder: WorkspaceFolder | undefined): Promise { if (!workspaceFolder) return let config = workspace.getConfiguration('extensions', workspaceFolder) let recommendations = toArray(config.inspect('recommendations').workspaceFolderValue) as string[] const unInstalled = recommendations.filter(name => !this.states.hasExtension(name)) let uri = workspaceFolder.uri if (!this.manager.states.shouldPrompt(uri) || unInstalled.length === 0) return let items = [{ title: `Install ${unInstalled.join(', ')}`, index: 1 }, { title: 'Don\'t show again', isCloseAffordance: true, index: 2 }] const item = await window.showInformationMessage(`Install recommend extensions?`, ...items) if (!item) return if (item.index === 1) { await this.installExtensions(unInstalled) } else { this.manager.states.addNoPromptFolder(uri) } } public getUpdateSettings(): UpdateSettings { let config = workspace.getConfiguration(null, null) let extensionsConfig = toObject>(config.inspect('extensions').globalValue) return { updateCheck: extensionsConfig.updateCheck ?? config.get('coc.preferences.extensionUpdateCheck', 'never'), updateUIInTab: extensionsConfig.updateUIInTab ?? config.get('coc.preferences.extensionUpdateUIInTab', false), silentAutoupdate: extensionsConfig.silentAutoupdate ?? config.get('coc.preferences.silentAutoupdate', true) } } public async init(runtimepath: string): Promise { if (process.env.COC_NO_PLUGINS == '1') return let stats = await this.globalPromise this.manager.registerExtensions(stats) let localStats = this.runtimeExtensionStats(runtimepath.split(',')) this.manager.registerExtensions(localStats) void this.manager.loadFileExtensions() } public async activateExtensions(): Promise { await this.manager.activateExtensions() if (process.env.COC_NO_PLUGINS == '1') { logger.warn('Extensions disabled by env COC_NO_PLUGINS') return } let names = this.states.filterGlobalExtensions(workspace.env.globalExtensions) void this.installExtensions(names) // check extensions need watch & install let settings = this.getUpdateSettings() if (this.states.shouldUpdate(settings.updateCheck)) { this.outputChannel.appendLine('Start auto update...') this.updateExtensions(settings.silentAutoupdate, settings.updateUIInTab).catch(e => { this.outputChannel.appendLine(`Error on updateExtensions ${e}`) }) } } public get onDidLoadExtension(): Event> { return this.manager.onDidLoadExtension } public get onDidActiveExtension(): Event> { return this.manager.onDidActiveExtension } public get onDidUnloadExtension(): Event { return this.manager.onDidUnloadExtension } private get outputChannel(): OutputChannel { return window.createOutputChannel('extensions') } /** * Get all loaded extensions. */ public get all(): Extension[] { return this.manager.all } public has(id: string): boolean { return this.manager.has(id) } public getExtension(id: string): ExtensionItem | undefined { return this.manager.getExtension(id) } public getExtensionById(extensionId: string): Extension | undefined { let item = this.manager.getExtension(extensionId) return item ? item.extension : undefined } /** * @deprecated Used by old version coc-json. */ public get schemes(): { [key: string]: PropertyScheme } { return {} } /** * @deprecated Used by old version coc-json. */ public addSchemeProperty(key: string, def: PropertyScheme): void { // workspace.configurations.extendsDefaults({ [key]: def.default }, id) } /** * @public Get state of extension */ public getExtensionState(id: string): ExtensionState { return this.manager.getExtensionState(id) } public isActivated(id: string): boolean { let item = this.manager.getExtension(id) return item != null && item.extension.isActive } public async call(id: string, method: string, args: any[]): Promise { return await this.manager.call(id, method, args) } public get npm(): string { let npm = workspace.initialConfiguration.get('npm.binPath') npm = workspace.expand(npm) for (let exe of [npm, 'npm']) { if (executable(exe)) return which.sync(exe) } void window.showErrorMessage(`Can't find ${npm} or npm in your $PATH`) return null } private createInstallerUI(isUpdate: boolean, silent: boolean, updateUIInTab: boolean): InstallUI { return silent ? new InstallChannel({ isUpdate }, this.outputChannel) : new InstallBuffer({ isUpdate, updateUIInTab }) } public createInstaller(npm: string, def: string): IInstaller { return new Installer(this.modulesFolder, npm, def) } /** * Install extensions, can be called without initialize. */ public async installExtensions(list: string[]): Promise { if (isFalsyOrEmpty(list) || !this.npm) return let { npm } = this list = distinct(list) let installBuffer = this.createInstallerUI(false, false, false) await Promise.resolve(installBuffer.start(list)) let fn = async (key: string): Promise => { try { installBuffer.startProgress(key) let installer = this.createInstaller(npm, key) installer.on('message', (msg, isProgress) => { installBuffer.addMessage(key, msg, isProgress) }) let result = await installer.install() installBuffer.finishProgress(key, true) this.states.addExtension(result.name, result.url ? result.url : `>=${result.version}`) let ms = key.match(/@[\d.]+$/) if (ms != null) this.states.setLocked(result.name, true) await this.manager.loadExtension(result.folder) } catch (err: any) { installBuffer.addMessage(key, err.message) installBuffer.finishProgress(key, false) void window.showErrorMessage(`Error on install ${key}: ${err}`) logger.error(`Error on install ${key}`, err) } } await concurrent(list, fn) } /** * Update global extensions */ public async updateExtensions(silent = false, updateUIInTab = false): Promise { let { npm } = this if (!npm) return let stats = this.globalExtensionStats() stats = stats.filter(s => { if (s.isLocked || s.state === 'disabled') { this.outputChannel.appendLine(`Skipped update for ${s.isLocked ? 'locked' : 'disabled'} extension "${s.id}"`) return false } return true }) this.states.setLastUpdate() this.cleanModulesFolder() let installBuffer = this.createInstallerUI(true, silent, updateUIInTab) await Promise.resolve(installBuffer.start(stats.map(o => o.id))) let fn = async (stat: ExtensionInfo): Promise => { let { id } = stat try { installBuffer.startProgress(id) let url = stat.exotic ? stat.uri : null let installer = this.createInstaller(npm, id) installer.on('message', (msg, isProgress) => { installBuffer.addMessage(id, msg, isProgress) }) let directory = await installer.update(url) installBuffer.finishProgress(id, true) if (directory) await this.manager.loadExtension(directory) } catch (err: any) { installBuffer.addMessage(id, err.message) installBuffer.finishProgress(id, false) void window.showErrorMessage(`Error on update ${id}: ${err}`) logger.error(`Error on update ${id}`, err) } } await concurrent(stats, fn, silent ? 1 : 3) } /** * Get all extension states */ public async getExtensionStates(): Promise { let runtimepaths = await workspace.nvim.runtimePaths let localStats = this.runtimeExtensionStats(runtimepaths) let globalStats = this.globalExtensionStats() return localStats.concat(globalStats) } public async globalExtensions(): Promise { if (process.env.COC_NO_PLUGINS == '1') return [] let res: ExtensionToLoad[] = [] for (let key of this.states.activated()) { let root = path.join(this.modulesFolder, key) try { let json = await loadGlobalJsonAsync(root, VERSION) res.push({ root, isLocal: false, packageJSON: json }) } catch (err) { logger.error(`Error on load package.json of ${key}`, err) } } return res } public globalExtensionStats(): ExtensionInfo[] { let dependencies = this.states.dependencies let lockedExtensions = this.states.lockedExtensions let infos: ExtensionInfo[] = [] Object.entries(dependencies).map(([key, val]) => { let root = path.join(this.modulesFolder, key) let errors: string[] = [] let obj = loadExtensionJson(root, VERSION, errors) if (errors.length > 0) { this.outputChannel.appendLine(`Error on load ${key} at ${root}: ${errors.join('\n')}`) return } obj.name = key infos.push({ id: key, root, isLocal: false, version: obj.version, description: obj.description ?? '', isLocked: lockedExtensions.includes(key), exotic: /^https?:/.test(val), uri: toUrl(val), state: this.getExtensionState(key), packageJSON: obj }) }) logger.debug('globalExtensionStats:', infos.length) return infos } public runtimeExtensionStats(runtimepaths: string[]): ExtensionInfo[] { let lockedExtensions = this.states.lockedExtensions let infos: ExtensionInfo[] = [] let localIds: Set = new Set() runtimepaths.map(root => { let errors: string[] = [] let obj = loadExtensionJson(root, workspace.version, errors) if (errors.length > 0) return let { name } = obj if (!name || this.states.hasExtension(name) || localIds.has(name)) return this.states.addLocalExtension(name, root) localIds.add(name) infos.push(({ id: obj.name, isLocal: true, isLocked: lockedExtensions.includes(name), version: obj.version, description: obj.description ?? '', exotic: false, root, state: this.getExtensionState(obj.name), packageJSON: Object.freeze(obj) })) }) return infos } /** * Remove unnecessary folders in node_modules */ public cleanModulesFolder(): void { let globalIds = this.states.globalIds let folders = globalIds.map(s => s.replace(/\/.*$/, '')) if (!fs.existsSync(this.modulesFolder)) return let files = fs.readdirSync(this.modulesFolder) for (let file of files) { if (folders.includes(file)) continue let p = path.join(this.modulesFolder, file) let stat = fs.lstatSync(p) if (stat.isSymbolicLink()) { fs.unlinkSync(p) } else if (stat.isDirectory()) { fs.rmSync(p, { recursive: true, force: true }) } } } public dispose(): void { this.manager.dispose() } } export function toUrl(val: string): string { return isUrl(val) ? val.replace(/\.git(#master|#main)?$/, '') : '' } export default new Extensions() ================================================ FILE: src/extension/installer.ts ================================================ 'use strict' import { EventEmitter } from 'events' import { URL } from 'url' import { v4 as uuid } from 'uuid' import { createLogger } from '../logger' import download, { DownloadOptions } from '../model/download' import fetch, { FetchOptions } from '../model/fetch' import { loadJson } from '../util/fs' import { child_process, fs, os, path, readline, semver } from '../util/node' import { toText } from '../util/string' import workspace from '../workspace' const logger = createLogger('extension-installer') const local_dependencies = ['coc.nvim', 'esbuild', 'webpack', '@types/node'] export interface Info { 'dist.tarball'?: string 'engines.coc'?: string version?: string name?: string } export type Dependencies = Record export interface InstallResult { name: string folder: string updated: boolean version: string url?: string } export function registryUrl(home = os.homedir()): URL { let res: URL let filepath = path.join(home, '.npmrc') if (fs.existsSync(filepath)) { try { let content = fs.readFileSync(filepath, 'utf8') let uri: string for (let line of content.split(/\r?\n/)) { if (line.startsWith('#')) continue let ms = line.match(/^(.*?)=(.*)$/) if (ms && ms[1] === 'coc.nvim:registry') { uri = ms[2] } } if (uri) res = new URL(uri) } catch (e) { logger.debug('Error on parse .npmrc:', e) } } return res ?? new URL('https://registry.npmjs.org') } export function isNpmCommand(exePath: string): boolean { let name = path.basename(exePath) return name === 'npm' || name === 'npm.CMD' } export function isYarn(exePath: string) { let name = path.basename(exePath) return ['yarn', 'yarn.CMD', 'yarnpkg', 'yarnpkg.CMD'].includes(name) } function isPnpm(exePath: string) { let name = path.basename(exePath) return name === 'pnpm' || name === 'pnpm.CMD' } function isSymbolicLink(folder: string): boolean { if (fs.existsSync(folder)) { let stat = fs.lstatSync(folder) if (stat.isSymbolicLink()) { return true } } return false } export interface IInstaller { on(event: 'message', cb: (msg: string, isProgress: boolean) => void): void install(): Promise update(url?: string): Promise } export class Installer extends EventEmitter implements IInstaller { private name: string private url: string private version: string constructor( private root: string, private npm: string, // could be url or name@version or name private def: string ) { super() if (/^https?:/.test(def)) { this.url = def } else { let ms = def.match(/(.+)@([^/]+)$/) if (ms) { this.name = ms[1] this.version = ms[2] } else { this.name = def } } } public get info() { return { name: this.name, version: this.version } } public async getInfo(): Promise { if (this.url) return await this.getInfoFromUri() let registry = registryUrl() this.log(`Get info from ${registry}`) let buffer = await this.fetch(new URL(this.name, registry), { timeout: 10000, buffer: true }) let res = JSON.parse(buffer.toString()) if (!this.version) this.version = res['dist-tags']['latest'] let obj = res['versions'][this.version] if (!obj) throw new Error(`${this.def} doesn't exists in ${registry}.`) let requiredVersion = obj['engines'] && obj['engines']['coc'] if (!requiredVersion) throw new Error(`${this.def} is not a valid coc extension, "engines" field with coc property required.`) return { 'dist.tarball': obj['dist']['tarball'], 'engines.coc': requiredVersion, version: obj['version'], name: res.name } as Info } public async getInfoFromUri(): Promise { let { url } = this if (!url.startsWith('https://github.com')) { throw new Error(`"${url}" is not supported, coc.nvim support github.com only`) } url = url.replace(/\/$/, '') let branch = 'master' if (url.includes('@')) { // https://github.com/sdras/vue-vscode-snippets@main let idx = url.indexOf('@') branch = url.substr(idx + 1) url = url.substring(0, idx) } let fileUrl = url.replace('github.com', 'raw.githubusercontent.com') + `/${branch}/package.json` this.log(`Get info from ${fileUrl}`) let content = await this.fetch(fileUrl, { timeout: 10000 }) let obj = typeof content == 'string' ? JSON.parse(content) : content this.name = obj.name return { 'dist.tarball': `${url}/archive/${branch}.tar.gz`, 'engines.coc': obj['engines'] ? obj['engines']['coc'] : null, name: obj.name, version: obj.version } } private log(msg: string, isProgress = false): void { this.emit('message', msg, isProgress) } public async install(): Promise { this.log(`Using npm from: ${this.npm}`) let info = await this.getInfo() logger.info(`Fetched info of ${this.def}`, info) let { name, version } = info let required = toText(info['engines.coc']).replace(/^\^/, '>=') if (required && !semver.satisfies(workspace.version, required)) { throw new Error(`${name} ${info.version} requires coc.nvim >= ${required}, please update coc.nvim.`) } let updated = await this.doInstall(info, new Set()) return { name, updated, version, url: this.url, folder: path.join(this.root, info.name) } } public async update(url?: string): Promise { if (url) this.url = url let version: string | undefined if (this.name) { let folder = path.join(this.root, this.name) if (isSymbolicLink(folder)) { this.log(`Skipped update for symbol link`) return } let obj = loadJson(path.join(folder, 'package.json')) as any version = obj.version } this.log(`Using npm from: ${this.npm}`) let info = await this.getInfo() if (version && info.version && semver.gte(version, info.version)) { this.log(`Current version ${version} is up to date.`) return } let required = info['engines.coc'] ? info['engines.coc'].replace(/^\^/, '>=') : '' if (required && !semver.satisfies(workspace.version, required)) { throw new Error(`${info.version} requires coc.nvim ${required}, please update coc.nvim.`) } let succeed = await this.doInstall(info, new Set()) if (!succeed) return let jsonFile = path.join(this.root, info.name, 'package.json') this.log(`Updated to v${info.version}`) return path.dirname(jsonFile) } public getInstallArguments(exePath: string, url: string | undefined): { env: string, args: string[] } { let env = 'production' let args = ['install', '--ignore-scripts'] if (url && url.startsWith('https://github.com')) { args = ['install'] env = 'development' } else { if (isNpmCommand(exePath)) { args.push('--no-package-lock') args.push('--omit=dev') args.push('--legacy-peer-deps') args.push('--no-global') } if (isYarn(exePath)) { args.push('--no-lockfile') args.push('--production') args.push('--ignore-engines') } if (isPnpm(exePath)) { args.push('--no-lockfile') args.push('--production') args.push('--config.strict-peer-dependencies=false') } } return { env, args } } private readLines(key: string, stream: NodeJS.ReadableStream): void { const rl = readline.createInterface({ input: stream }) rl.on('line', line => { this.log(`${key} ${line}`, true) }) } public installDependencies(folder: string, dependencies: string[]): Promise { if (dependencies.length == 0) return Promise.resolve() return new Promise((resolve, reject) => { let { env, args } = this.getInstallArguments(this.npm, this.url) this.log(`Installing dependencies by: ${this.npm} ${args.join(' ')}.`) const cmd = process.platform === 'win32' && this.npm.includes(' ') ? `"${this.npm}"` : this.npm const child = child_process.spawn(cmd, args, { cwd: folder, shell: process.platform === 'win32', env: Object.assign(process.env, { NODE_ENV: env }) }) this.readLines('[npm stdout]', child.stdout) this.readLines('[npm stderr]', child.stderr) child.stderr.setEncoding('utf8') child.stdout.setEncoding('utf8') child.on('error', reject) child.on('exit', code => { if (code) { reject(new Error(`${this.npm} install exited with ${code}`)) return } resolve() }) }) } public async doInstall(info: Info, installing: Set = new Set()): Promise { let dest = path.join(this.root, info.name) if (isSymbolicLink(dest)) return false if (installing.has(info.name)) { this.log(`Skipping circular dependency: ${info.name}`) return false } installing.add(info.name) let key = info.name.replace(/\//g, '_') let downloadFolder = path.join(this.root, `${key}-${uuid()}`) let url = info['dist.tarball'] this.log(`Downloading from ${url}`) let etagAlgorithm = url.startsWith('https://registry.npmjs.org') ? 'md5' : undefined let obj: any try { await this.download(url, { dest: downloadFolder, etagAlgorithm, extract: 'untar', onProgress: p => this.log(`Download progress ${p}%`, true), }) this.log(`Extension download at ${downloadFolder}`) obj = loadJson(path.join(downloadFolder, 'package.json')) as any await this.installDependencies(downloadFolder, getDependencies(obj)) } catch (e) { fs.rmSync(downloadFolder, { recursive: true, force: true }) throw e } this.log(`Download extension ${info.name}@${info.version} at ${downloadFolder}`) fs.mkdirSync(path.dirname(dest), { recursive: true }) if (fs.existsSync(dest)) fs.rmSync(dest, { force: true, recursive: true }) fs.renameSync(downloadFolder, dest) this.log(`Move extension ${info.name}@${info.version} to ${dest}`) const extensionDependencies = getExtensionDependencies(obj) if (extensionDependencies.length > 0) { this.log(`Installing extension dependencies: ${extensionDependencies.join(', ')}`) for (const dependency of extensionDependencies) { const installer = new Installer(this.root, this.npm, dependency) installer.on('message', (msg, isProgress) => { this.log(msg, isProgress) }) await installer.doInstall(await installer.getInfo(), installing) } } return true } public async download(url: string, options: DownloadOptions): Promise { return await download(url, options) } public async fetch(url: string | URL, options: FetchOptions = {}): Promise { return await fetch(url, options) } } export function getDependencies(obj: { dependencies?: { [key: string]: string } }): string[] { return Object.keys(obj.dependencies ?? {}).filter(id => !local_dependencies.includes(id)) } export function getExtensionDependencies(obj: { extensionDependencies?: string[] }): string[] { if (obj.extensionDependencies?.length > 0) { return [...new Set(obj.extensionDependencies)] } return [] } ================================================ FILE: src/extension/manager.ts ================================================ import { URI } from 'vscode-uri' import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../configuration/registry' import { ConfigurationScope } from '../configuration/types' import events from '../events' import { createLogger } from '../logger' import Memos from '../model/memos' import { disposeAll, wait } from '../util' import { splitArray, toArray } from '../util/array' import { configHome, dataHome } from '../util/constants' import { onUnexpectedError } from '../util/errors' import { Extensions as ExtensionsInfo, IExtensionRegistry, IStringDictionary, getProperties } from '../util/extensionRegistry' import { ExtensionExport, createExtension } from '../util/factory' import { isDirectory, loadJson, remove, statAsync, watchFile } from '../util/fs' import * as Is from '../util/is' import type { IJSONSchema } from '../util/jsonSchema' import { omit } from '../util/lodash' import { path } from '../util/node' import { deepClone, deepIterate, isEmpty } from '../util/object' import { Disposable, Emitter, Event } from '../util/protocol' import { Registry, convertProperties } from '../util/registry' import { createTiming } from '../util/timing' import window from '../window' import workspace from '../workspace' import { ExtensionJson, ExtensionStat, getJsFiles, loadExtensionJson, validExtensionFolder } from './stat' interface ExportExtension { readonly name: string readonly isActive: boolean unload: () => Promise /** * API returned by activate function */ readonly api: any /** * The object of module.exports of the extension entry without activate & deactivate function. */ readonly exports: any } export type ExtensionState = 'disabled' | 'loaded' | 'activated' | 'unknown' const logger = createLogger('extensions-manager') export enum ExtensionType { Global, Local, SingleFile, Internal } export enum ActivateEvents { OnLanguage = 'onLanguage', OnFileSystem = 'onFileSystem', OnCommand = 'onCommand', WorkspaceContains = 'workspaceContains', } export interface ExtensionInfo { id: string version?: string description?: string root: string exotic: boolean uri?: string state: ExtensionState isLocal: boolean isLocked: boolean packageJSON: Readonly } export type ExtensionToLoad = Pick, 'root' | 'packageJSON' | 'isLocal'> export interface Extension { readonly id: string readonly extensionPath: string readonly extensionUri: URI readonly isActive: boolean readonly packageJSON: ExtensionJson readonly exports: T readonly module: object activate(): Promise } export type API = { [index: string]: any } | void | null | undefined export interface ExtensionItem { readonly id: string readonly type: ExtensionType readonly events: ReadonlyArray extension: Extension deactivate: () => void | Promise filepath?: string directory: string readonly isLocal: boolean } const extensionRegistry = Registry.as(ExtensionsInfo.ExtensionContribution) const memos = new Memos(path.resolve(dataHome, 'memos.json')) memos.merge(path.resolve(dataHome, '../memos.json')) const configurationRegistry = Registry.as(Extensions.Configuration) /** * Manage loaded extensions */ export class ExtensionManager { private activated = false private disposables: Disposable[] = [] public readonly configurationNodes: IConfigurationNode[] = [] private extensions: Map = new Map() private _onDidLoadExtension = new Emitter>() private _onDidActiveExtension = new Emitter>() private _onDidUnloadExtension = new Emitter() private singleExtensionsRoot = path.join(configHome, 'coc-extensions') private modulesFolder: string public readonly onDidLoadExtension: Event> = this._onDidLoadExtension.event public readonly onDidActiveExtension: Event> = this._onDidActiveExtension.event public readonly onDidUnloadExtension: Event = this._onDidUnloadExtension.event constructor(public readonly states: ExtensionStat, private folder: string) { this.modulesFolder = path.join(this.folder, 'node_modules') } public activateExtensions(): Promise[]> { this.activated = true if (process.env.COC_NO_PLUGINS == '1') return configurationRegistry.registerConfigurations(this.configurationNodes) this.attachEvents() let promises: Promise[] = [] for (let key of this.extensions.keys()) { // wait extensions that always activated only const { extension } = this.extensions.get(key) const activationEvents = extension.packageJSON.activationEvents if (!activationEvents || activationEvents.includes('*')) { promises.push(void this.activate(key)) } else { void this.autoActivate(key, extension) } } return Promise.allSettled(promises) } public async loadFileExtensions(): Promise { let folder = this.singleExtensionsRoot let files = await getJsFiles(folder) await Promise.allSettled(files.map(file => { return this.loadExtensionFile(path.join(folder, file)) })) } public attachEvents(): void { workspace.onDidRuntimePathChange(async paths => { let folders = paths.filter(p => p && validExtensionFolder(p, workspace.version)) let outputChannel = window.createOutputChannel('extensions') await Promise.allSettled(folders.map(folder => { outputChannel.appendLine(`Loading extension from runtimepath: ${folder}`) return this.loadExtension(folder) })) }, null, this.disposables) workspace.onDidOpenTextDocument(document => { let doc = workspace.getDocument(document.bufnr) this.tryActivateExtensions(ActivateEvents.OnLanguage, events => { return checkLanguageId(doc, events) }) this.tryActivateExtensions(ActivateEvents.OnFileSystem, events => { return checkFileSystem(doc.uri, events) }) }, null, this.disposables) events.on('Command', async command => { let fired = false this.tryActivateExtensions(ActivateEvents.OnCommand, events => { let result = checkCommand(command, events) if (result) fired = true return result }) if (fired) await wait(50) }, null, this.disposables) workspace.onDidChangeWorkspaceFolders(e => { if (e.added.length > 0) { this.tryActivateExtensions(ActivateEvents.WorkspaceContains, events => { let patterns = toWorkspaceContainsPatterns(events) return workspace.checkPatterns(patterns, e.added) }) } }, null, this.disposables) } /** * Unload & remove all global extensions, return removed extensions. */ public async cleanExtensions(): Promise { let { globalIds } = this.states await remove(this.modulesFolder) return globalIds.filter(id => !this.states.isDisabled(id)) } public tryActivateExtensions(event: string, check: (activationEvents: string[]) => boolean | Promise): void { for (let item of this.extensions.values()) { if (item.extension.isActive) continue let events = item.events if (!events.includes(event)) continue let { extension } = item let activationEvents = getActivationEvents(extension.packageJSON) void Promise.resolve(check(activationEvents)).then(checked => { if (checked) void Promise.resolve(this.activate(extension.id)) }) } } private async checkAutoActivate(packageJSON: ExtensionJson): Promise { let activationEvents = getActivationEvents(packageJSON) if (activationEvents.length === 0 || activationEvents.includes('*')) { return true } let patterns: string[] = [] for (let eventName of activationEvents as string[]) { let parts = eventName.split(':') let ev = parts[0] if (ev === ActivateEvents.OnLanguage) { if (workspace.languageIds.has(parts[1]) || workspace.filetypes.has(parts[1])) { return true } } else if (ev === ActivateEvents.WorkspaceContains && parts[1]) { patterns.push(parts[1]) } else if (ev === ActivateEvents.OnFileSystem) { for (let doc of workspace.documents) { let u = URI.parse(doc.uri) if (u.scheme == parts[1]) { return true } } } } if (patterns.length > 0) { let res = await workspace.checkPatterns(patterns) if (res) return true } return false } public has(id: string): boolean { return this.extensions.has(id) } public getExtension(id: string): ExtensionItem | undefined { return this.extensions.get(id) } public get loadedExtensions(): string[] { return Array.from(this.extensions.keys()) } public get all(): Extension[] { return Array.from(this.extensions.values()).map(o => o.extension) } /** * Activate extension, throw error if disabled or doesn't exist. * Returns true if extension successfully activated. */ public async activate(id: string, activating: Set = new Set()): Promise { let item = this.extensions.get(id) if (!item) throw new Error(`Extension ${id} not registered!`) let { extension } = item if (extension.isActive) return true if (activating.has(id)) { logger.warn(`Circular dependency detected: ${id}`) return false } activating = new Set([...activating, id]) const { packageJSON } = extension if (packageJSON.extensionDependencies?.length > 0) { const results = await Promise.allSettled(packageJSON.extensionDependencies.map(dep => this.activate(dep, activating))) for (const result of results) { if (result.status === 'rejected' || (result.status === 'fulfilled' && !result.value)) { logger.error(`Could not activate dependency for ${id}, activation failed.`) return false } } } await extension.activate() return extension.isActive === true } public async deactivate(id: string): Promise { let item = this.extensions.get(id) if (!item || !item.extension.isActive) return await Promise.resolve(item.deactivate()) } /** * Load extension from folder, folder should contains coc extension. */ public async loadExtension(folder: string | string[], noActive = false): Promise { if (Array.isArray(folder)) { let results = await Promise.allSettled(folder.map(f => { return this.loadExtension(f, noActive) })) results.forEach(res => { if (res.status === 'rejected') throw new Error(`Error on loadExtension ${res.reason}`) }) return true } let errors: string[] = [] let obj = loadExtensionJson(folder, workspace.version, errors) if (errors.length > 0) throw new Error(errors[0]) let { name } = obj if (this.states.isDisabled(name)) return false // unload if loaded await this.unloadExtension(name) let isLocal = !this.states.hasExtension(name) if (isLocal) this.states.addLocalExtension(name, folder) await this.registerExtension(folder, Object.freeze(obj), isLocal ? ExtensionType.Local : ExtensionType.Global, noActive) return true } /** * Deactivate & unregist extension */ public async unloadExtension(id: string): Promise { let item = this.extensions.get(id) if (item) { await this.deactivate(id) this.extensions.delete(id) this._onDidUnloadExtension.fire(id) } } public async reloadExtension(id: string): Promise { let item = this.extensions.get(id) if (!item || item.type == ExtensionType.Internal) { throw new Error(`Extension ${id} not registered`) } if (item.type == ExtensionType.SingleFile) { await this.loadExtensionFile(item.filepath) } else { await this.loadExtension(item.directory) } } public async call(id: string, method: string, args: any[]): Promise { let item = this.extensions.get(id) if (!item) throw new Error(`extension ${id} not registered`) let { extension } = item if (!extension.isActive) { await this.activate(id) } let { exports } = extension if (!exports || typeof exports[method] !== 'function') { throw new Error(`method ${method} not found on extension ${id}`) } return await Promise.resolve(exports[method].apply(null, args)) } public registContribution(id: string, packageJSON: any, directory: string, filepath?: string): void { let { contributes, activationEvents } = packageJSON let { configuration, rootPatterns, commands } = contributes ?? {} let definitions: IStringDictionary | undefined let props = getProperties(configuration ?? {}) if (!isEmpty(props)) { // /configuration let properties = convertProperties(props, ConfigurationScope.WINDOW) if (Is.objectLiteral(configuration.definitions)) { let prefix = id.replace(/[^\w]/g, '') const addPrefix = (obj: object, key: string) => { if (key == '$ref') { let val = obj[key] if (Is.string(val) && val.startsWith('#/definitions/')) { obj[key] = val.slice(0, 14) + prefix + '.' + val.slice(14) } } } deepIterate(properties, addPrefix) definitions = {} Object.entries(deepClone(configuration.definitions)).forEach(([key, val]) => { if (Is.objectLiteral(val)) { definitions[prefix + '.' + key] = val deepIterate(val, addPrefix) } }) } let node: IConfigurationNode = { properties, extensionInfo: { id, displayName: packageJSON.displayName } } this.configurationNodes.push(node) if (this.activated) { let toRemove = [] let idx = this.configurationNodes.findIndex(o => o.extensionInfo!.id === id) if (idx !== -1) { toRemove.push(this.configurationNodes[idx]) this.configurationNodes.splice(idx, 1) } workspace.configurations.updateConfigurations([node], toRemove) } } extensionRegistry.registerExtension(id, { name: id, directory, filepath, commands, definitions, rootPatterns, onCommands: getOnCommandList(activationEvents) }) } public getExtensionState(id: string): ExtensionState { let disabled = this.states.isDisabled(id) if (disabled) return 'disabled' let item = this.getExtension(id) if (!item) return 'unknown' let { extension } = item return extension.isActive ? 'activated' : 'loaded' } public async autoActivate(id: string, extension: Extension): Promise { try { let checked = await this.checkAutoActivate(extension.packageJSON) if (checked) await Promise.resolve(this.activate(id)) } catch (e) { logger.error(`Error on activate ${id}`, e) } } public async loadExtensionFile(filepath: string, noActive = false): Promise { let stat = await statAsync(filepath) if (!stat || !stat.isFile()) return let filename = path.basename(filepath) let basename = path.basename(filepath, '.js') let name = 'single-' + basename let root = path.dirname(filepath) let packageJSON = { name, main: filename, engines: { coc: '>=0.0.82' } } let confpath = path.join(root, basename + '.json') let obj = loadJson(confpath) as any for (const attr of ['activationEvents', 'contributes']) { packageJSON[attr] = obj[attr] } await this.unloadExtension(name) await this.registerExtension(root, packageJSON, ExtensionType.SingleFile, noActive) return name } public registerExtensions(stats: ExtensionToLoad[]): void { for (let stat of stats) { try { let extensionType = stat.isLocal ? ExtensionType.Local : ExtensionType.Global void this.registerExtension(stat.root, stat.packageJSON, extensionType) } catch (e) { logger.error(`Error on regist extension from ${stat.root}: `, e) } } } public async registerExtension(root: string, packageJSON: ExtensionJson, extensionType: ExtensionType, noActive = false): Promise { let id = packageJSON.name if (this.states.isDisabled(id)) return let isActive = false let result: Promise | undefined let filename = path.join(root, packageJSON.main || 'index.js') let extensionPath = extensionType === ExtensionType.SingleFile ? filename : root let exports: any let ext: ExtensionExport let subscriptions: Disposable[] = [] const timing = createTiming(`activate ${id}`, 5000) let extension: Extension = { activate: (): Promise => { if (result) return result result = new Promise(async (resolve, reject) => { timing.start() try { let isEmpty = typeof packageJSON.engines.coc === 'undefined' ext = createExtension(id, filename, isEmpty) let context = { subscriptions, extensionPath, globalState: memos.createMemento(`${id}|global`), workspaceState: memos.createMemento(`${id}|${workspace.rootPath}`), asAbsolutePath: relativePath => path.join(root, relativePath), storagePath: path.join(this.folder, `${id}-data`), logger: createLogger(`extension:${id}`) } let res = await Promise.resolve(ext.activate(context)) isActive = true exports = res this._onDidActiveExtension.fire(extension) timing.stop() resolve(res) } catch (e) { logger.error(`Error on active extension ${id}:`, e) reject(e as Error) } }) return result }, id, packageJSON, extensionPath, extensionUri: URI.parse(extensionPath), get isActive() { return isActive }, get module() { return ext }, get exports() { if (!isActive) throw new Error(`Invalid access to exports, extension "${id}" not activated`) return exports } } Object.freeze(extension) this.extensions.set(id, { id, type: extensionType, isLocal: extensionType == ExtensionType.Local, extension, directory: root, filepath: filename, events: getEvents(packageJSON.activationEvents), deactivate: async () => { if (!isActive) return isActive = false result = undefined exports = undefined disposeAll(subscriptions) if (ext && typeof ext.deactivate === 'function') { try { await Promise.resolve(ext.deactivate()) ext = undefined } catch (e) { logger.error(`Error on ${id} deactivate: `, e) } } } }) this.registContribution(id, packageJSON, root, filename) this._onDidLoadExtension.fire(extension) if (this.activated && !noActive) await this.autoActivate(id, extension) } public unregistContribution(id: string): void { let idx = this.configurationNodes.findIndex(o => o.extensionInfo!.id === id) extensionRegistry.unregistExtension(id) if (idx !== -1) { let node = this.configurationNodes[idx] this.configurationNodes.splice(idx, 1) configurationRegistry.deregisterConfigurations([node]) } } public async registerInternalExtension(extension: Extension, deactivate?: () => void): Promise { let { id, packageJSON } = extension this.extensions.set(id, { id, directory: __dirname, type: ExtensionType.Internal, events: getEvents(packageJSON.activationEvents), extension, deactivate, isLocal: true }) this.registContribution(id, packageJSON, __dirname) this._onDidLoadExtension.fire(extension) await this.autoActivate(id, extension) } /** * Only global extensions can be uninstalled */ public async uninstallExtensions(ids: string[]): Promise { let [globals, filtered] = splitArray(ids, id => this.states.hasExtension(id)) for (let id of globals) { await this.unloadExtension(id) this.states.removeExtension(id) extensionRegistry.unregistExtension(id) await remove(path.join(this.modulesFolder, id)) } if (filtered.length > 0) { void window.showWarningMessage(`Global extensions ${filtered.join(', ')} not found`) } if (globals.length > 0) { void window.showInformationMessage(`Removed extensions: ${globals.join(' ')}`) } } public async toggleExtension(id: string): Promise { let state = this.getExtensionState(id) if (state == 'activated') await this.deactivate(id) if (state != 'disabled') { this.states.setDisable(id, true) this.unregistContribution(id) await this.unloadExtension(id) } else { this.states.setDisable(id, false) if (id.startsWith('single-')) { let filepath = path.join(this.singleExtensionsRoot, `${id.replace(/^single-/, '')}.js`) await this.loadExtensionFile(filepath) } else { let folder = this.states.getFolder(id) if (folder) { await this.loadExtension(folder) } else { void window.showWarningMessage(`Extension ${id} not found`) } } } } public async watchExtension(id: string): Promise { let item = this.getExtension(id) if (!item) throw new Error(`extension ${id} not found`) if (id.startsWith('single-')) { void window.showInformationMessage(`watching ${item.filepath}`) this.disposables.push(watchFile(item.filepath, async () => { await this.loadExtensionFile(item.filepath) void window.showInformationMessage(`reloaded ${id}`) }, global.__TEST__ === true)) } else { let client = await workspace.fileSystemWatchers.createClient(item.directory, true) if (!client) throw new Error('watchman not found') void window.showInformationMessage(`watching ${item.directory}`) client.subscribe('**/*.js', async () => { this.reloadExtension(id).then(() => { void window.showInformationMessage(`reloaded ${id}`) }, onUnexpectedError) }) } } /** * load extension in folder or file */ public async load(filepath: string, active: boolean): Promise { let name: string if (isDirectory(filepath)) { let obj = loadJson(path.join(filepath, 'package.json')) as any name = obj.name await this.loadExtension(filepath, true) } else { name = await this.loadExtensionFile(filepath, true) } if (!name) throw new Error(`Unable to load extension at ${filepath}`) let disabled = this.states.isDisabled(name) if (disabled) throw new Error(`extension ${name} is disabled`) let item = this.getExtension(name) if (active) await this.activate(name) return { get isActive() { return item.extension.isActive }, get name() { return name }, get api() { return item.extension.exports }, get exports() { let module = item.extension.module ?? {} return omit(module, ['activate']) }, unload: () => { return this.unloadExtension(name) } } } public dispose(): void { disposeAll(this.disposables) } } export function getEvents(activationEvents: string[] | undefined): string[] { let res: string[] = [] for (let ev of toArray(activationEvents)) { let [name] = ev.split(':', 2) if (name && !res.includes(name)) res.push(name) } return res } export function getOnCommandList(activationEvents: string[] | undefined): string[] { let res: string[] = [] for (let ev of toArray(activationEvents)) { let [name, command] = ev.split(':', 2) if (name === ActivateEvents.OnCommand && command) res.push(command) } return res } export function checkLanguageId(document: { languageId: string, filetype: string }, activationEvents: string[]): boolean { for (let eventName of activationEvents as string[]) { let parts = eventName.split(':') let ev = parts[0] if (ev == ActivateEvents.OnLanguage && (document.languageId == parts[1] || document.filetype == parts[1])) { return true } } return false } export function checkCommand(command: string, activationEvents: string[]): boolean { for (let eventName of activationEvents as string[]) { let parts = eventName.split(':') let ev = parts[0] if (ev == ActivateEvents.OnCommand && command == parts[1]) { return true } } return false } export function checkFileSystem(uri: string, activationEvents: string[]): boolean { let scheme = URI.parse(uri).scheme for (let eventName of activationEvents as string[]) { let parts = eventName.split(':') let ev = parts[0] if (ev == ActivateEvents.OnFileSystem && scheme == parts[1]) { return true } } return false } export function getActivationEvents(json: ExtensionJson): string[] { return toArray(json.activationEvents).filter(key => typeof key === 'string' && key.length > 0) } /** * Convert globl patterns */ export function toWorkspaceContainsPatterns(activationEvents: string[]): string[] { let patterns: string[] = [] for (let eventName of activationEvents) { let parts = eventName.split(':') if (parts[0] == ActivateEvents.WorkspaceContains && parts[1]) { patterns.push(parts[1]) } } return patterns } ================================================ FILE: src/extension/stat.ts ================================================ import { createLogger } from '../logger' import { toArray } from '../util/array' import { readFile, writeJson } from '../util/fs' import { objectLiteral } from '../util/is' import { fs, path, promisify, semver } from '../util/node' import { toObject } from '../util/object' const logger = createLogger('extension-stat') interface DataBase { extension?: { [key: string]: { disabled?: boolean locked?: boolean } }, lastUpdate?: number } interface PackageJson { disabled?: string[] locked?: string[] lastUpdate?: number dependencies?: { [key: string]: string } } export interface ExtensionJson { name: string main?: string engines: { [key: string]: string } activationEvents?: string[] extensionDependencies?: string[] version?: string [key: string]: any } export enum ExtensionStatus { Normal, Disabled, Locked, } const ONE_DAY = 24 * 60 * 60 * 1000 const DISABLE_PROMPT_KEY = 'disablePrompt' /** * Stat for global extensions */ export class ExtensionStat { private disabled: Set = new Set() private locked: Set = new Set() private extensions: Set = new Set() private localExtensions: Map = new Map() constructor(private folder: string) { try { this.migrate() } catch (e) { logger.error(`Error on update package.json at ${folder}`, e) } } private migrate(): void { let curr = loadJson(this.jsonFile) as PackageJson let db = path.join(this.folder, 'db.json') let changed = false if (fs.existsSync(db)) { let obj = loadJson(db) as DataBase let def = obj.extension ?? {} for (let [key, o] of Object.entries(def)) { if (o.disabled) this.disabled.add(key) if (o.locked) this.locked.add(key) } curr.disabled = Array.from(this.disabled) curr.locked = Array.from(this.locked) curr.lastUpdate = obj.lastUpdate fs.unlinkSync(db) changed = true } else { this.disabled = new Set(curr.disabled ?? []) this.locked = new Set(curr.locked ?? []) } if (changed) writeJson(this.jsonFile, curr) let ids = Object.keys(curr.dependencies ?? {}) this.extensions = new Set(ids) } public addNoPromptFolder(uri: string): void { let curr = loadJson(this.jsonFile) as PackageJson curr[DISABLE_PROMPT_KEY] = curr[DISABLE_PROMPT_KEY] ?? [] curr[DISABLE_PROMPT_KEY].push(uri) writeJson(this.jsonFile, curr) } public shouldPrompt(uri: string): boolean { let curr = loadJson(this.jsonFile) as PackageJson let arr = curr[DISABLE_PROMPT_KEY] as string[] ?? [] return !arr.includes(uri) } public reset(): void { writeJson(this.jsonFile, {}) } public *activated(): Iterable { let { disabled } = this for (let key of Object.keys(this.dependencies)) { if (!disabled.has(key)) { yield key } } } public addLocalExtension(name: string, folder: string): void { this.localExtensions.set(name, folder) } public getFolder(name: string): string | undefined { if (this.extensions.has(name)) return path.join(this.folder, 'node_modules', name) return this.localExtensions.get(name) } public getExtensionsStat(): Record { let res: Record = {} for (let id of this.extensions) { if (this.disabled.has(id)) { res[id] = ExtensionStatus.Disabled } else if (this.locked.has(id)) { res[id] = ExtensionStatus.Locked } else { res[id] = ExtensionStatus.Normal } } return res } public hasExtension(id: string): boolean { return this.extensions.has(id) } public addExtension(id: string, val: string): void { let curr = loadJson(this.jsonFile) as PackageJson curr.dependencies = curr.dependencies ?? {} curr.dependencies[id] = val this.extensions.add(id) writeJson(this.jsonFile, curr) } public removeExtension(id: string): void { let curr = loadJson(this.jsonFile) as PackageJson if (curr.disabled) curr.disabled = curr.disabled.filter(key => key !== id) if (curr.locked) curr.locked = curr.locked.filter(key => key !== id) curr.dependencies = curr.dependencies ?? {} delete curr.dependencies[id] this.extensions.delete(id) writeJson(this.jsonFile, curr) } public isDisabled(id: string): boolean { return this.disabled.has(id) } public get lockedExtensions(): string[] { return Array.from(this.locked) } public get disabledExtensions(): string[] { return Array.from(this.disabled) } public get dependencies(): { [key: string]: string } { let curr = loadJson(this.jsonFile) as PackageJson return curr.dependencies ?? {} } public setDisable(id: string, disable: boolean): void { if (disable) { this.disabled.add(id) } else { this.disabled.delete(id) } this.update('disabled', Array.from(this.disabled)) } public setLocked(id: string, locked: boolean): void { if (locked) { this.locked.add(id) } else { this.locked.delete(id) } this.update('locked', Array.from(this.disabled)) } public setLastUpdate(): void { this.update('lastUpdate', Date.now()) } public shouldUpdate(opt: string): boolean { if (opt === 'never') return false let interval = toInterval(opt) let curr = loadJson(this.jsonFile) as PackageJson return curr.lastUpdate == null || (Date.now() - curr.lastUpdate) > interval } public get globalIds(): ReadonlyArray { let curr = loadJson(this.jsonFile) as PackageJson return Object.keys(curr.dependencies ?? {}) } /** * Filter out global extensions that needs install */ public filterGlobalExtensions(names: string[] | undefined): string[] { let disabledExtensions = this.disabledExtensions let dependencies = this.dependencies let map: Map = new Map() toArray(names).forEach(def => { if (!def || typeof def !== 'string') return let name = getExtensionName(def) map.set(name, def) }) let currentUrls: string[] = [] let exists: string[] = [] for (let [key, val] of Object.entries(dependencies)) { if (fs.existsSync(path.join(this.folder, 'node_modules', key, 'package.json'))) { exists.push(key) if (typeof val === 'string' && /^https?:/.test(val)) { currentUrls.push(val) } } } for (let name of map.keys()) { if (disabledExtensions.includes(name) || this.extensions.has(name)) { map.delete(name) continue } if ((/^https?:/.test(name) && currentUrls.some(url => url.startsWith(name))) || exists.includes(name)) { map.delete(name) } } return Array.from(map.values()) } private update(key: keyof PackageJson, value: any): void { let curr = loadJson(this.jsonFile) as PackageJson curr[key] = value writeJson(this.jsonFile, curr) } private get jsonFile(): string { return path.join(this.folder, 'package.json') } } export function toInterval(opt: string): number { return opt === 'daily' ? ONE_DAY : ONE_DAY * 7 } export function validExtensionFolder(folder: string, version: string): boolean { let errors: string[] = [] let res = loadExtensionJson(folder, version, errors) return res != null && errors.length == 0 } function getEntryFile(main: string | undefined): string { if (!main) return 'index.js' if (!main.endsWith('.js')) return main + '.js' return main } export async function loadGlobalJsonAsync(folder: string, version: string): Promise { let jsonFile = path.join(folder, 'package.json') let content = await readFile(jsonFile, 'utf8') let packageJSON = JSON.parse(content) as ExtensionJson let { engines } = packageJSON let main = getEntryFile(packageJSON.main) if (!engines || (typeof engines.coc !== 'string' && typeof engines.vscode !== 'string')) throw new Error('Invalid engines field') let keys = Object.keys(engines) if (keys.includes('coc') && !semver.satisfies(version, engines['coc'].replace(/^\^/, '>='))) { throw new Error(`coc.nvim version not match, required ${engines['coc']}`) } if (!engines.vscode && !fs.existsSync(path.join(folder, main))) { throw new Error(`main file ${main} not found, you may need to build the project.`) } return packageJSON } export function loadExtensionJson(folder: string, version: string, errors: string[]): ExtensionJson | undefined { let jsonFile = path.join(folder, 'package.json') if (!fs.existsSync(jsonFile)) { errors.push(`package.json not found in ${folder}`) return undefined } let packageJSON = loadJson(jsonFile) as ExtensionJson let { name, engines } = packageJSON let main = getEntryFile(packageJSON.main) if (!name) errors.push(`can't find name in package.json`) if (!engines || !objectLiteral(engines)) { errors.push(`invalid engines in ${jsonFile}`) } if (engines && !engines.vscode && !fs.existsSync(path.join(folder, main))) { errors.push(`main file ${main} not found, you may need to build the project.`) } if (engines) { let keys = Object.keys(engines) if (!keys.includes('coc') && !keys.includes('vscode')) { errors.push(`Engines in package.json doesn't have coc or vscode`) } if (keys.includes('coc')) { let required = engines['coc'].replace(/^\^/, '>=') if (!semver.satisfies(version, required)) { errors.push(`Please update coc.nvim, ${packageJSON.name} requires coc.nvim ${engines['coc']}`) } } } return packageJSON } /** * Name of extension */ export function getExtensionName(def: string): string { if (/^https?:/.test(def)) return def if (!def.includes('@')) return def return def.replace(/@[\d.]+$/, '') } export function checkExtensionRoot(root: string): boolean { try { if (!fs.existsSync(root)) { fs.mkdirSync(root, { recursive: true }) } let stat = fs.statSync(root) if (!stat.isDirectory()) { logger.info(`Trying to delete ${root}`) fs.unlinkSync(root) fs.mkdirSync(root, { recursive: true }) } let jsonFile = path.join(root, 'package.json') if (!fs.existsSync(jsonFile)) { fs.writeFileSync(jsonFile, '{"dependencies":{}}', 'utf8') } } catch (e) { console.error(`Unexpected error when check data home ${root}: ${e}`) return false } return true } export async function getJsFiles(folder: string): Promise { if (!fs.existsSync(folder)) return [] let files = await promisify(fs.readdir)(folder) return files.filter(f => f.endsWith('.js')) } function loadJson(filepath: string): object { try { let text = fs.readFileSync(filepath, 'utf8') let data = JSON.parse(text) return toObject(data) } catch (e) { logger.error(`Error on parse json file ${filepath}`, e) return {} } } ================================================ FILE: src/extension/ui.ts ================================================ 'use strict' import events from '../events' import { frames } from '../model/status' import { HighlightItem, OutputChannel } from '../types' import { disposeAll, getConditionValue } from '../util' import { debounce } from '../util/node' import { Disposable } from '../util/protocol' import { byteLength } from '../util/string' import window from '../window' import workspace from '../workspace' const interval = getConditionValue(100, 1) export enum State { Waiting, Failed, Progressing, Success, } interface InstallSettings { isUpdate: boolean updateUIInTab?: boolean } export interface InstallUI { start(names: string[]): void | Promise addMessage(name: string, msg: string, isProgress?: boolean): void startProgress(name: string): void finishProgress(name: string, succeed?: boolean): void } export class InstallChannel implements InstallUI { constructor(private settings: InstallSettings, private channel: OutputChannel) { } private get isUpdate(): boolean { return this.settings.isUpdate } public getText(): string { return this.isUpdate ? 'update' : 'install' } public start(names: string[]): void { this.channel.appendLine(`${this.isUpdate ? 'Updating' : 'Installing'} ${names.join(', ')}`) } public addMessage(name: string, msg: string, isProgress?: boolean): void { if (!isProgress) { this.channel.appendLine(`${name} - ${msg}`) } } public startProgress(name: string): void { this.channel.appendLine(`Start ${this.getText()} ${name}`) } public finishProgress(name: string, succeed?: boolean): void { if (succeed) { this.channel.appendLine(`${name} ${this.getText()} succeed!`) } else { this.channel.appendLine(`${name} ${this.getText()} failed!`) } } } const debounceTime = getConditionValue(500, 10) export class InstallBuffer implements InstallUI { private statMap: Map = new Map() private updated: Set = new Set() private messagesMap: Map = new Map() private disposables: Disposable[] = [] private names: string[] = [] private interval: NodeJS.Timeout public bufnr: number constructor(private settings: InstallSettings) { let floatFactory = window.createFloatFactory({ modes: ['n'] }) this.disposables.push(floatFactory) let fn = debounce(async (bufnr, cursor) => { if (bufnr == this.bufnr) { let msgs = this.getMessages(cursor[0] - 1) let docs = msgs.length > 0 ? [{ content: msgs.join('\n'), filetype: 'txt' }] : [] await floatFactory.show(docs) } }, debounceTime) this.disposables.push(Disposable.create(() => { fn.clear() })) events.on('CursorMoved', fn, this.disposables) events.on('BufUnload', bufnr => { if (bufnr === this.bufnr) { this.dispose() } }, null, this.disposables) } public async start(names: string[]): Promise { this.statMap.clear() this.names = names for (let name of names) { this.statMap.set(name, State.Waiting) } await this.show() } public addMessage(name: string, msg: string): void { let lines = this.messagesMap.get(name) || [] this.messagesMap.set(name, lines.concat(msg.trim().split(/\r?\n/))) if ((msg.startsWith('Updated to') || msg.startsWith('Installed extension'))) { this.updated.add(name) } } public startProgress(name: string): void { this.statMap.set(name, State.Progressing) } public finishProgress(name: string, succeed?: boolean): void { this.statMap.set(name, succeed ? State.Success : State.Failed) } public get remains(): number { let count = 0 for (let name of this.names) { let stat = this.statMap.get(name) if (![State.Success, State.Failed].includes(stat)) { count = count + 1 } } return count } private getLinesAndHighlights(start: number): { lines: string[], highlights: HighlightItem[] } { let lines: string[] = [] let highlights: HighlightItem[] = [] for (let name of this.names) { let state = this.statMap.get(name) let processText = '*' let hlGroup: string | undefined let lnum = start + lines.length switch (state) { case State.Progressing: { let d = new Date() let idx = Math.floor(d.getMilliseconds() / 100) processText = frames[idx] hlGroup = undefined break } case State.Failed: processText = '✗' hlGroup = 'ErrorMsg' break case State.Success: processText = '✓' hlGroup = this.updated.has(name) ? 'MoreMsg' : 'Comment' break } let msgs = this.messagesMap.get(name) || [] let pre = `- ${processText} ` let len = byteLength(pre) if (hlGroup) { highlights.push({ hlGroup, lnum, colStart: len, colEnd: len + byteLength(name) }) } lines.push(`${pre}${name} ${msgs.length ? msgs[msgs.length - 1] : ''}`) } return { lines, highlights } } public getMessages(line: number): string[] { let name = this.names[line - 2] return this.messagesMap.get(name) ?? [] } public get stopped(): boolean { return this.interval == null } private get isUpdate(): boolean { return this.settings.isUpdate } // draw frame public draw(): void { let { remains, bufnr } = this let { nvim } = workspace if (!bufnr) return let buffer = nvim.createBuffer(bufnr) let first = remains == 0 ? `${this.isUpdate ? 'Update' : 'Install'} finished` : `Installing, ${remains} remaining...` let { lines, highlights } = this.getLinesAndHighlights(2) nvim.pauseNotification() buffer.setLines([first, '', ...lines], { start: 0, end: -1, strictIndexing: false }, true) buffer.updateHighlights('coc-extensions', highlights, { priority: 99 }) if (remains == 0 && this.interval) { clearInterval(this.interval) this.interval = null } nvim.resumeNotification(true, true) } private highlight(): void { let { nvim } = workspace nvim.call('matchadd', ['CocListFgCyan', '^\\-\\s\\zs\\*'], true) nvim.call('matchadd', ['CocListFgGreen', '^\\-\\s\\zs✓'], true) nvim.call('matchadd', ['CocListFgRed', '^\\-\\s\\zs✗'], true) } private async show(): Promise { let isSync = events.requesting === true let { nvim } = workspace nvim.pauseNotification() nvim.command(isSync ? 'enew' : (this.settings.updateUIInTab ? 'tabnew' : 'vs +enew'), true) nvim.call('bufnr', ['%'], true) nvim.command('setl buftype=nofile bufhidden=wipe noswapfile nobuflisted wrap undolevels=-1', true) if (!isSync) nvim.command('nnoremap q :q', true) this.highlight() let res = await nvim.resumeNotification() this.bufnr = res[0][1] as number this.interval = setInterval(() => { this.draw() }, interval) } public dispose(): void { this.bufnr = undefined this.messagesMap.clear() this.statMap.clear() disposeAll(this.disposables) clearInterval(this.interval) this.interval = null } } ================================================ FILE: src/handler/callHierarchy.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { TextDocument } from 'vscode-languageserver-textdocument' import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Position, Range } from 'vscode-languageserver-types' import commands from '../commands' import events from '../events' import languages, { ProviderName } from '../languages' import { TreeDataProvider } from '../tree/index' import LocationsDataProvider from '../tree/LocationsDataProvider' import BasicTreeView from '../tree/TreeView' import { IConfigurationChangeEvent } from '../types' import { disposeAll } from '../util' import { isFalsyOrEmpty } from '../util/array' import { omit } from '../util/lodash' import { CancellationToken, CancellationTokenSource, Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' interface CallHierarchyDataItem extends CallHierarchyItem { parent?: CallHierarchyDataItem ranges?: Range[] sourceUri?: string children?: CallHierarchyItem[] } interface CallHierarchyConfig { splitCommand: string openCommand: string enableTooltip: boolean } enum ShowHierarchyAction { Incoming = 'Show Incoming Calls', Outgoing = 'Show Outgoing Calls' } interface CallHierarchyProvider extends TreeDataProvider { meta: 'incoming' | 'outgoing' dispose: () => void } /** * Cleanup properties used by treeview */ function toCallHierarchyItem(item: CallHierarchyDataItem): CallHierarchyItem { return omit(item, ['children', 'parent', 'ranges', 'sourceUri']) } function isCallHierarchyItem(item: any): item is CallHierarchyItem { if (item && typeof item.name === 'string' && item.kind && Range.is(item.range)) return true return false } const HIGHLIGHT_GROUP = 'CocSelectedRange' export default class CallHierarchyHandler { private config: CallHierarchyConfig private disposables: Disposable[] = [] public static commandId = 'callHierarchy.reveal' private highlightWinids: Set = new Set() constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) this.disposables.push(commands.registerCommand(CallHierarchyHandler.commandId, async (winid: number, item: CallHierarchyDataItem, openCommand?: string) => { let { nvim } = this await nvim.call('win_gotoid', [winid]) await workspace.jumpTo(item.uri, item.selectionRange.start, openCommand) let win = await nvim.window win.clearMatchGroup(HIGHLIGHT_GROUP) win.highlightRanges(HIGHLIGHT_GROUP, [item.selectionRange], 10, true) if (isFalsyOrEmpty(item.ranges)) return if (item.sourceUri) { let doc = workspace.getDocument(item.sourceUri) if (!doc) return let winid = await nvim.call('coc#compat#buf_win_id', [doc.bufnr]) as number if (winid == -1) return if (winid != win.id) { win = nvim.createWindow(winid) win.clearMatchGroup(HIGHLIGHT_GROUP) } } win.highlightRanges(HIGHLIGHT_GROUP, item.ranges, 100, true) this.highlightWinids.add(win.id) }, null, true)) events.on('BufWinEnter', (_, winid) => { if (this.highlightWinids.has(winid)) { this.highlightWinids.delete(winid) let win = nvim.createWindow(winid) win.clearMatchGroup(HIGHLIGHT_GROUP) } }, null, this.disposables) commands.register({ id: 'document.showIncomingCalls', execute: async () => { await this.showCallHierarchyTree('incoming') } }, false, 'show incoming calls in tree view.') commands.register({ id: 'document.showOutgoingCalls', execute: async () => { await this.showCallHierarchyTree('outgoing') } }, false, 'show outgoing calls in tree view.') } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('callHierarchy')) { let c = workspace.getConfiguration('callHierarchy', null) this.config = { splitCommand: c.get('splitCommand'), openCommand: c.get('openCommand'), enableTooltip: c.get('enableTooltip') } } } private createProvider(rootItems: CallHierarchyDataItem[], doc: TextDocument, winid: number, kind: 'incoming' | 'outgoing'): CallHierarchyProvider { let provider = new LocationsDataProvider( kind, winid, this.config, CallHierarchyHandler.commandId, rootItems, kind => this.handler.getIcon(kind), (el, meta, token) => this.getChildren(doc, el, meta, token) ) for (let kind of ['incoming', 'outgoing']) { let name = kind === 'incoming' ? ShowHierarchyAction.Incoming : ShowHierarchyAction.Outgoing provider.addAction(name, (el: CallHierarchyDataItem) => { provider.meta = kind as 'incoming' | 'outgoing' let rootItems = [toCallHierarchyItem(el)] provider.reset(rootItems) }) } return provider } private async getChildren(doc: TextDocument, item: CallHierarchyDataItem, kind: 'incoming' | 'outgoing', token: CancellationToken): Promise { let items: CallHierarchyDataItem[] = [] let callHierarchyItem = toCallHierarchyItem(item) if (kind == 'incoming') { let res = await languages.provideIncomingCalls(doc, callHierarchyItem, token) if (res) items = res.map(o => Object.assign(o.from, { ranges: o.fromRanges })) } else { let res = await languages.provideOutgoingCalls(doc, callHierarchyItem, token) if (res) items = res.map(o => Object.assign(o.to, { ranges: o.fromRanges, sourceUri: item.uri })) } return items } private async prepare(doc: TextDocument, position: Position, token: CancellationToken): Promise { this.handler.checkProvider(ProviderName.CallHierarchy, doc) const res = await languages.prepareCallHierarchy(doc, position, token) return isCallHierarchyItem(res) ? [res] : res } private async getCallHierarchyItems(item: CallHierarchyItem | undefined, kind: 'outgoing'): Promise private async getCallHierarchyItems(item: CallHierarchyItem | undefined, kind: 'incoming'): Promise private async getCallHierarchyItems(item: CallHierarchyItem | undefined, kind: 'incoming' | 'outgoing'): Promise<(CallHierarchyIncomingCall | CallHierarchyOutgoingCall)[]> { const { doc, position } = await this.handler.getCurrentState() const source = new CancellationTokenSource() if (!item) { await doc.synchronize() let res = await this.prepare(doc.textDocument, position, source.token) item = res ? res[0] : undefined if (!res) throw new Error('Unable to getCallHierarchyItem at current position') } let method = kind == 'incoming' ? 'provideIncomingCalls' : 'provideOutgoingCalls' return await languages[method](doc.textDocument, item, source.token) } public async getIncoming(item?: CallHierarchyItem): Promise { return await this.getCallHierarchyItems(item, 'incoming') } public async getOutgoing(item?: CallHierarchyItem): Promise { return await this.getCallHierarchyItems(item, 'outgoing') } public async showCallHierarchyTree(kind: 'incoming' | 'outgoing'): Promise { const { doc, position, winid } = await this.handler.getCurrentState() await doc.synchronize() if (!languages.hasProvider(ProviderName.CallHierarchy, doc.textDocument)) { void window.showErrorMessage(`CallHierarchy provider not found for current document, it's not supported by your languageserver`) return } const res = await languages.prepareCallHierarchy(doc.textDocument, position, CancellationToken.None) const rootItems: CallHierarchyItem[] = isCallHierarchyItem(res) ? [res] : res if (isFalsyOrEmpty(rootItems)) { void window.showWarningMessage('Unable to get CallHierarchyItem at cursor position.') return } let provider = this.createProvider(rootItems, doc.textDocument, winid, kind) let treeView = new BasicTreeView('CALLS', { treeDataProvider: provider }) treeView.title = getTitle(kind) provider.onDidChangeTreeData(e => { if (!e) treeView.title = getTitle(provider.meta) }) treeView.onDidChangeVisibility(e => { if (!e.visible) provider.dispose() }) this.disposables.push(treeView) await treeView.show(this.config.splitCommand) } public dispose(): void { this.highlightWinids.clear() disposeAll(this.disposables) } } function getTitle(kind: string): string { return `${kind.toUpperCase()} CALLS` } ================================================ FILE: src/handler/codeActions.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { CodeAction, CodeActionContext, CodeActionKind, CodeActionTriggerKind, Range } from 'vscode-languageserver-types' import commandManager from '../commands' import diagnosticManager from '../diagnostic/manager' import languages from '../languages' import { createLogger } from '../logger' import Document from '../model/document' import { isFalsyOrEmpty } from '../util/array' import { boolToNumber } from '../util/numbers' import { CancellationToken, CancellationTokenSource } from '../util/protocol' import { createTiming } from '../util/timing' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' const logger = createLogger('handler-codeActions') /** * Handle codeActions related methods. */ export default class CodeActions { constructor( private nvim: Neovim, private handler: HandlerDelegate ) { handler.addDisposable(commandManager.registerCommand('editor.action.organizeImport', async () => { let succeed = await this.organizeImport() if (!succeed) void window.showWarningMessage(`Organize import action not found`) })) commandManager.titles.set('editor.action.organizeImport', 'Run organize import code action, show warning when not exists') handler.addDisposable(commandManager.registerCommand('editor.action.executeCodeActions', async (doc: Document, range: Range | undefined, actionKinds: CodeActionKind[], timeout: number) => { await this.executeCodeActions(doc, range, actionKinds, timeout) }, this, true)) } public async executeCodeActions(doc: Document, range: Range | undefined, actionKinds: CodeActionKind[], timeout: number): Promise { let timing = createTiming('Execute code action', timeout) let applied: string[] = [] for (const kind of actionKinds) { let codeActions = await this.getCodeActions(doc, range, [kind]) let codeAction = codeActions.find(o => !o.disabled) if (codeAction) { logger.info(`Apply code action "${kind}" to buffer ${doc.bufnr}`) timing.start(`"${kind}"`) let tokenSource = new CancellationTokenSource() let timer: NodeJS.Timeout let _resolve: (value: any) => void const tp = new Promise(c => { timer = setTimeout(() => { logger.warn(`Apply code action "${kind}" timeout after ${timeout}ms`) tokenSource.cancel() c(undefined) }, timeout) _resolve = c }) await Promise.race([tp, this.applyCodeAction(codeAction, tokenSource.token)]) if (!tokenSource.token.isCancellationRequested) { applied.push(kind) } timing.stop() clearTimeout(timer) _resolve(undefined) tokenSource.dispose() await doc.synchronize() } } return applied } public async codeActionRange(start: number, end: number, only?: string): Promise { let { doc } = await this.handler.getCurrentState() await doc.synchronize() let line = doc.getline(end - 1) let range = Range.create(start - 1, 0, end - 1, line.length) let codeActions = await this.getCodeActions(doc, range, only ? [only] : null) codeActions = codeActions.filter(o => !o.disabled) if (!codeActions || codeActions.length == 0) { void window.showWarningMessage(`No${only ? ' ' + only : ''} code action available`) return } let idx = await window.showMenuPicker(codeActions.map(o => o.title), 'Choose action') let action = codeActions[idx] if (action) await this.applyCodeAction(action) } public async organizeImport(): Promise { let { doc } = await this.handler.getCurrentState() await doc.synchronize() let actions = await this.getCodeActions(doc, undefined, [CodeActionKind.SourceOrganizeImports]) if (actions && actions.length) { await this.applyCodeAction(actions[0]) return true } return false } public async getCodeActions(doc: Document, range?: Range, only?: CodeActionKind[]): Promise { let excludeSourceAction = range !== null && (!only || only.findIndex(o => o.startsWith(CodeActionKind.Source)) == -1) range = range ?? Range.create(0, 0, doc.lineCount, 0) let diagnostics = diagnosticManager.getDiagnosticsInRange(doc.textDocument, range) let context: CodeActionContext = { diagnostics, triggerKind: CodeActionTriggerKind.Invoked } if (!isFalsyOrEmpty(only)) context.only = only let tokenSource = new CancellationTokenSource() let codeActions = await languages.getCodeActions(doc.textDocument, range, context, tokenSource.token) if (!codeActions || codeActions.length == 0) return [] if (excludeSourceAction) { codeActions = codeActions.filter(o => !o.kind || !o.kind.startsWith(CodeActionKind.Source)) } codeActions.sort((a, b) => { if (a.disabled && !b.disabled) return 1 if (b.disabled && !a.disabled) return -1 if (a.isPreferred != b.isPreferred) return boolToNumber(b.isPreferred) - boolToNumber(a.isPreferred) if (!only) { if (isQuickfix(a) && !isQuickfix(b)) return -1 if (isQuickfix(b) && !isQuickfix(a)) return 1 } return 0 }) return codeActions } private get floatActions(): boolean { return workspace.initialConfiguration.get('coc.preferences.floatActions', true) } public async doCodeAction(mode: string | null, only: CodeActionKind[] | string, showDisable = false): Promise { let { doc, position } = await this.handler.getCurrentState() let range: Range | undefined if (mode) { range = await window.getSelectedRange(mode) } else { range = Range.create(position, position) } await doc.synchronize() let codeActions = await this.getCodeActions(doc, range, Array.isArray(only) ? only : null) if (typeof only == 'string') { codeActions = codeActions.filter(o => o.title == only || (o.command && o.command.title == only)) } else if (Array.isArray(only)) { codeActions = codeActions.filter(o => only.some(k => o.kind && o.kind.startsWith(k))) } if (!this.floatActions || !showDisable) codeActions = codeActions.filter(o => !o.disabled) if (!codeActions || codeActions.length == 0) { void window.showWarningMessage(`No${only ? ' ' + only : ''} code action available`) return } if (codeActions.length == 1 && !codeActions[0].disabled && shouldAutoApply(only)) { await this.applyCodeAction(codeActions[0]) return } let idx = this.floatActions ? await window.showMenuPicker( codeActions.map(o => { return { text: o.title, disabled: o.disabled } }), 'Choose action' ) : await window.requestInputList('Choose action by number', codeActions.map(o => o.title)) let action = codeActions[idx] if (action) await this.applyCodeAction(action) } /** * Get current codeActions */ public async getCurrentCodeActions(mode?: string, only?: CodeActionKind[]): Promise { let { doc } = await this.handler.getCurrentState() let range: Range if (mode) range = await window.getSelectedRange(mode) let codeActions = await this.getCodeActions(doc, range, only) return codeActions.filter(o => !o.disabled) } /** * Invoke preferred quickfix at current position */ public async doQuickfix(): Promise { let actions = await this.getCurrentCodeActions('currline', [CodeActionKind.QuickFix]) if (!actions || actions.length == 0) { void window.showWarningMessage(`No quickfix action available`) return } await this.applyCodeAction(actions[0]) this.nvim.command(`silent! call repeat#set("\\(coc-fix-current)", -1)`, true) } public async applyCodeAction(action: CodeAction, token?: CancellationToken): Promise { if (action.disabled) { throw new Error(`Action "${action.title}" is disabled: ${action.disabled.reason}`) } token = token == null ? CancellationToken.None : token let resolved = await languages.resolveCodeAction(action, token) if (!resolved || token.isCancellationRequested) return let { edit, command } = resolved if (edit) await workspace.applyEdit(edit) if (command) await commandManager.execute(command) } } export function shouldAutoApply(only: CodeActionKind[] | string | undefined): boolean { if (!only) return false if (typeof only === 'string' || only[0] === CodeActionKind.QuickFix || only[0] === CodeActionKind.SourceFixAll) { return workspace.initialConfiguration.get('coc.preferences.autoApplySingleQuickfix', true) } return false } function isQuickfix(codeAction: CodeAction): boolean { return codeAction.kind && codeAction.kind.startsWith('quickfix') } ================================================ FILE: src/handler/codelens/buffer.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import { CodeLens, Command } from 'vscode-languageserver-types' import commandManager from '../../commands' import languages, { ProviderName } from '../../languages' import { createLogger } from '../../logger' import { SyncItem } from '../../model/bufferSync' import Document from '../../model/document' import { DidChangeTextDocumentParams } from '../../types' import { defaultValue, getConditionValue } from '../../util' import { isFalsyOrEmpty } from '../../util/array' import { onUnexpectedError } from '../../util/errors' import { isCommand } from '../../util/is' import { debounce } from '../../util/node' import { CancellationTokenSource } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' const logger = createLogger('codelens-buffer') export interface CodeLensInfo { codeLenses: CodeLens[] version: number } export interface CodeLensConfig { position: 'top' | 'eol' | 'right_align' enabled: boolean display: boolean separator: string subseparator: string } export enum TextAlign { After = 'after', Right = 'right', Below = 'below', Above = 'above', } let srcId: number | undefined const debounceTime = getConditionValue(200, 20) const CODELENS_HL = 'CocCodeLens' const NORMAL_HL = 'Normal' /** * CodeLens buffer */ export default class CodeLensBuffer implements SyncItem { private codeLenses: CodeLensInfo | undefined private tokenSource: CancellationTokenSource private resolveTokenSource: CancellationTokenSource private _config: CodeLensConfig | undefined public resolveCodeLens: (() => void) & { clear(): void } public debounceFetch: (() => void) & { clear(): void } constructor( private nvim: Neovim, public readonly document: Document ) { this.resolveCodeLens = debounce(() => { this._resolveCodeLenses().catch(onUnexpectedError) }, debounceTime) this.debounceFetch = debounce(() => { this.fetchCodeLenses().catch(onUnexpectedError) }, debounceTime) if (this.hasProvider) this.debounceFetch() } public get config(): CodeLensConfig { if (this._config) return this._config this.loadConfiguration() return this._config } public loadConfiguration(): void { let config = workspace.getConfiguration('codeLens', this.document) this._config = { enabled: config.get('enable', false), display: config.get('display', true), position: config.get<'top' | 'eol' | 'right_align'>('position', 'top'), separator: config.get('separator', ''), subseparator: config.get('subseparator', ' ') } } public async toggleDisplay(): Promise { if (!this.hasProvider || !this.config.enabled) return if (this.config.display) { this.config.display = false this.clear() } else { this.config.display = true this.resolveCodeLens.clear() await this._resolveCodeLenses() } } public get bufnr(): number { return this.document.bufnr } public onChange(e: DidChangeTextDocumentParams): void { if (e.contentChanges.length === 0 && this.codeLenses != null) { this.resolveCodeLens.clear() this._resolveCodeLenses().catch(onUnexpectedError) } else { this.cancel() this.debounceFetch() } } public get currentCodeLens(): CodeLens[] | undefined { return this.codeLenses?.codeLenses } private get hasProvider(): boolean { return languages.hasProvider(ProviderName.CodeLens, this.document) } public async forceFetch(): Promise { if (!this.config.enabled || !this.hasProvider) return await this.document.synchronize() this.cancel() await this.fetchCodeLenses() } public async fetchCodeLenses(): Promise { if (!this.hasProvider || !this.config.enabled) return let noFetch = this.codeLenses?.version == this.document.version if (!noFetch) { let empty = this.codeLenses == null let { textDocument } = this.document let version = textDocument.version this.cancelFetch() let tokenSource = this.tokenSource = new CancellationTokenSource() let token = tokenSource.token if (!srcId) srcId = await this.nvim.createNamespace('coc-codelens') let codeLenses = await languages.getCodeLens(textDocument, token) if (token.isCancellationRequested) return codeLenses = defaultValue(codeLenses, []) codeLenses = codeLenses.filter(o => o != null) if (isFalsyOrEmpty(codeLenses)) { this.clear() return } this.codeLenses = { version, codeLenses } if (empty) this.setVirtualText(codeLenses) } this.resolveCodeLens.clear() await this._resolveCodeLenses() } /** * Resolve visible codeLens */ private async _resolveCodeLenses(): Promise { if (!this.codeLenses || this.isChanged) return let { codeLenses } = this.codeLenses let [bufnr, start, end, total] = await this.nvim.eval(`[bufnr('%'),line('w0'),line('w$'),line('$')]`) as [number, number, number, number] // only resolve current buffer if (this.isChanged || bufnr != this.bufnr) return this.cancel() codeLenses = codeLenses.filter(o => { let lnum = o.range.start.line + 1 return lnum >= start && lnum <= end }) if (codeLenses.length) { let tokenSource = this.resolveTokenSource = new CancellationTokenSource() let token = tokenSource.token await Promise.all(codeLenses.map(codeLens => { if (isCommand(codeLens.command)) return Promise.resolve() codeLens.command = undefined return languages.resolveCodeLens(codeLens, token) })) this.resolveTokenSource = undefined if (token.isCancellationRequested || this.isChanged) return } // nvim could have extmarks exceeded last line. if (end == total) end = -1 this.nvim.pauseNotification() this.clear() this.setVirtualText(codeLenses) this.nvim.resumeNotification(true, true) } private get isChanged(): boolean { if (!this.codeLenses || this.document.dirty) return true let { version } = this.codeLenses return this.document.textDocument.version !== version } /** * Attach resolved codeLens */ private setVirtualText(codeLenses: CodeLens[]): void { let { document } = this if (!srcId || !document || !codeLenses.length || !this.config.display) return let top = this.config.position === 'top' let list: Map = new Map() for (let codeLens of codeLenses) { let { line } = codeLens.range.start let curr = list.get(line) ?? [] curr.push(codeLens) list.set(line, curr) } for (let lnum of list.keys()) { let codeLenses = list.get(lnum) let commands = codeLenses.reduce((p, c) => { if (c && c.command && c.command.title) p.push(c.command.title.replace(/\s+/g, ' ')) return p }, [] as string[]) let chunks: [string, string][] = [] let len = commands.length for (let i = 0; i < len; i++) { let title = commands[i] chunks.push([title, CODELENS_HL] as [string, string]) if (i != len - 1) { chunks.push([this.config.subseparator, CODELENS_HL] as [string, string]) } } if (chunks.length > 0 && this.config.separator) { chunks.unshift([`${this.config.separator} `, CODELENS_HL]) } if (top && chunks.length == 0) { chunks.push([' ', NORMAL_HL]) } if (chunks.length > 0) { document.buffer.setVirtualText(srcId, lnum, chunks, { text_align: getTextAlign(this.config.position), indent: true }) } } } public clear(start = 0, end = -1): void { if (!srcId) return let buf = this.nvim.createBuffer(this.bufnr) buf.clearNamespace(srcId, start, end) } public async doAction(line: number): Promise { let commands = getCommands(line, this.codeLenses?.codeLenses) if (commands.length == 1) { await commandManager.execute(commands[0]) } else if (commands.length > 1) { let res = await window.showMenuPicker(commands.map(c => c.title)) if (res != -1) await commandManager.execute(commands[res]) } } private cancelFetch(): void { this.debounceFetch.clear() if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } private cancelResolve(): void { if (this.resolveTokenSource) { this.resolveTokenSource.cancel() this.resolveTokenSource = null } } private cancel(): void { this.resolveCodeLens.clear() this.cancelResolve() this.cancelFetch() } public abandonResult(): void { this.codeLenses = undefined } public dispose(): void { this.cancel() this.codeLenses = undefined } } export function getTextAlign(position: 'top' | 'eol' | 'right_align'): TextAlign { if (position == 'top') return TextAlign.Above if (position == 'eol') return TextAlign.After if (position === 'right_align') return TextAlign.Right return TextAlign.Above } export function getCommands(line: number, codeLenses: CodeLens[] | undefined): Command[] { if (!codeLenses?.length) return [] let commands: Command[] = [] for (let codeLens of codeLenses) { let { range, command } = codeLens if (!isCommand(command)) continue if (line == range.start.line) { commands.push(command) } } return commands } ================================================ FILE: src/handler/codelens/index.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import type { DocumentSelector } from 'vscode-languageserver-protocol' import { debounce } from '../..//util/node' import commands from '../../commands' import events from '../../events' import languages from '../../languages' import BufferSync from '../../model/bufferSync' import { disposeAll, getConditionValue } from '../../util' import { Disposable } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' import CodeLensBuffer from './buffer' const debounceTime = getConditionValue(200, 0) /** * Show codeLens of document */ export default class CodeLensManager { private disposables: Disposable[] = [] public buffers: BufferSync constructor(private nvim: Neovim) { workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('codeLens')) { for (let item of this.buffers.items) { item.loadConfiguration() } } }, this, this.disposables) this.buffers = workspace.registerBufferSync(doc => { if (doc.buftype != '') return undefined return new CodeLensBuffer(nvim, doc) }) this.disposables.push(this.buffers) events.on('CursorHold', async (bufnr: number) => { let item = this.buffers.getItem(bufnr) if (item && item.config.enabled && !item.currentCodeLens) await item.forceFetch() }, null, this.disposables) events.on('CursorMoved', bufnr => { let buf = this.buffers.getItem(bufnr) if (buf) buf.resolveCodeLens() }, null, this.disposables) let debounced = debounce(async (selector: DocumentSelector) => { for (let item of this.buffers.items) { if (!workspace.match(selector, item.document)) continue item.abandonResult() await item.forceFetch() } }, debounceTime) this.disposables.push(Disposable.create(() => { debounced.clear() })) languages.onDidCodeLensRefresh(debounced, null, this.disposables) commands.register({ id: 'document.toggleCodeLens', execute: () => { return this.toggle(workspace.bufnr) }, }, false, 'toggle codeLens display of current buffer') } public async toggle(bufnr: number): Promise { let item = this.buffers.getItem(bufnr) try { workspace.getAttachedDocument(bufnr) await item.toggleDisplay() } catch (e) { void window.showErrorMessage((e as Error).message) } } /** * Check provider for buf that not fetched */ public async checkProvider(): Promise { for (let buf of this.buffers.items) { await buf.forceFetch() } } public async doAction(): Promise { let [bufnr, line] = await this.nvim.eval(`[bufnr("%"),line(".")-1]`) as [number, number] let buf = this.buffers.getItem(bufnr) if (buf) await buf.doAction(line) } public dispose(): void { disposeAll(this.disposables) } } ================================================ FILE: src/handler/colors/colorBuffer.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import { Color, ColorInformation, Position, Range } from 'vscode-languageserver-types' import languages, { ProviderName } from '../../languages' import { SyncItem } from '../../model/bufferSync' import Document from '../../model/document' import { HighlightItem } from '../../types' import { getConditionValue } from '../../util' import { isDark, toHexString } from '../../util/color' import * as Is from '../../util/is' import { debounce } from '../../util/node' import { comparePosition, positionInRange } from '../../util/position' import { CancellationTokenSource } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' const NAMESPACE = 'color' export interface ColorRanges { color: Color ranges: Range[] } export interface ColorConfig { filetypes: string[] | null highlightPriority: number } const debounceTime = getConditionValue(200, 10) export default class ColorBuffer implements SyncItem { private _colors: ColorInformation[] = [] private tokenSource: CancellationTokenSource | undefined public highlight: (() => void) & { clear(): void } private _enable: boolean | undefined // last highlight version constructor( private nvim: Neovim, public readonly doc: Document, private config: ColorConfig, private usedColors: Set) { this.highlight = debounce(() => { void this.doHighlight() }, debounceTime) if (this.hasProvider) this.highlight() } public get enable(): boolean { if (Is.boolean(this._enable)) return this._enable this._enable = workspace.getConfiguration('colors', this.doc).get('enable', false) return this._enable } public updateDocumentConfig(): void { let enable = this.enabled this._enable = workspace.getConfiguration('colors', this.doc).get('enable', false) if (enable != this.enabled) { if (enable) { this.clearHighlight() } else { void this.doHighlight() } } } public toggle(): void { if (this._enable) { this._enable = false this.clearHighlight() } else { this._enable = true void this.doHighlight() } } private get hasProvider(): boolean { return languages.hasProvider(ProviderName.DocumentColor, this.doc) } public get enabled(): boolean { let { filetypes } = this.config let { filetype } = this.doc if (!this.hasProvider) return false if (Array.isArray(filetypes) && (filetypes.includes('*') || filetypes.includes(filetype))) return true return this.enable } public onChange(): void { this.cancel() this.highlight() } public get buffer(): Buffer { return this.doc.buffer } public get colors(): ColorInformation[] { return this._colors } public hasColor(): boolean { return this._colors.length > 0 } public async doHighlight(): Promise { if (!this.enabled) return let { nvim, doc } = this this.tokenSource = new CancellationTokenSource() let { token } = this.tokenSource let colors: ColorInformation[] colors = await languages.provideDocumentColors(doc.textDocument, token) if (token.isCancellationRequested) return colors = colors || [] colors.sort((a, b) => comparePosition(a.range.start, b.range.start)) this._colors = colors let items: HighlightItem[] = [] colors.forEach(o => { let hlGroup = getHighlightGroup(o.color) doc.addHighlights(items, hlGroup, o.range, { combine: false }) }) let diff = await window.diffHighlights(doc.bufnr, NAMESPACE, items) if (token.isCancellationRequested || !diff) return nvim.pauseNotification() this.defineColors(colors) nvim.resumeNotification(false, true) await window.applyDiffHighlights(doc.bufnr, NAMESPACE, this.config.highlightPriority, diff, true) } private defineColors(colors: ColorInformation[]): void { for (let color of colors) { let hex = toHexString(color.color) if (!this.usedColors.has(hex)) { this.nvim.command(`hi BG${hex} guibg=#${hex} guifg=#${isDark(color.color) ? 'ffffff' : '000000'}`, true) this.usedColors.add(hex) } } } public hasColorAtPosition(position: Position): boolean { return this.colors.some(o => positionInRange(position, o.range) == 0) } public clearHighlight(): void { this.highlight.clear() this._colors = [] this.buffer.clearNamespace('color') } public cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } public dispose(): void { this._colors = [] this.highlight.clear() this.cancel() } } function getHighlightGroup(color: Color): string { return `BG${toHexString(color)}` } ================================================ FILE: src/handler/colors/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { ColorInformation, Position } from 'vscode-languageserver-types' import commandManager from '../../commands' import events from '../../events' import languages, { ProviderName } from '../../languages' import BufferSync from '../../model/bufferSync' import { IConfigurationChangeEvent } from '../../types' import { defaultValue, disposeAll } from '../../util' import { toHexString } from '../../util/color' import { CancellationTokenSource, Disposable } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' import { HandlerDelegate } from '../types' import ColorBuffer, { ColorConfig } from './colorBuffer' export default class Colors { private config: ColorConfig private disposables: Disposable[] = [] private highlighters: BufferSync constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) let usedColors: Set = new Set() this.highlighters = workspace.registerBufferSync(doc => { return new ColorBuffer(this.nvim, doc, this.config, usedColors) }) events.on('ColorScheme', () => { usedColors.clear() for (let item of this.highlighters.items) { item.cancel() void item.doHighlight() } }, null, this.disposables) languages.onDidColorsRefresh(selector => { for (let item of this.highlighters.items) { if (workspace.match(selector, item.doc)) { item.highlight() } } }) commandManager.register({ id: 'editor.action.pickColor', execute: async () => { await this.pickColor() } }, false, 'pick color from system color picker when possible.') commandManager.register({ id: 'editor.action.colorPresentation', execute: async () => { await this.pickPresentation() } }, false, 'change color presentation.') commandManager.register({ id: 'document.toggleColors', execute: async () => { let bufnr = await nvim.call('bufnr', ['%']) as number let item = this.highlighters.getItem(bufnr) workspace.getAttachedDocument(bufnr) item.toggle() } }, false, 'toggle colors for current buffer') } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('colors')) { let c = workspace.initialConfiguration.get('colors') as any this.config = Object.assign(this.config ?? {}, { filetypes: c.filetypes, highlightPriority: defaultValue(c.highlightPriority, 1000) }) if (e) { for (let item of this.highlighters.items) { item.updateDocumentConfig() } } } } public async pickPresentation(): Promise { let { doc } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.DocumentColor, doc.textDocument) let info = await this.getColorInformation(doc.bufnr) if (!info) return void window.showWarningMessage('Color not found at current position') let tokenSource = new CancellationTokenSource() let presentations = await languages.provideColorPresentations(info, doc.textDocument, tokenSource.token) if (!presentations?.length) return void window.showWarningMessage('No color presentations found') let res = await window.showMenuPicker(presentations.map(o => o.label), 'Choose color:') if (res == -1) return let presentation = presentations[res] let { textEdit, additionalTextEdits, label } = presentation if (!textEdit) textEdit = { range: info.range, newText: label } await doc.applyEdits([textEdit]) if (additionalTextEdits) await doc.applyEdits(additionalTextEdits) } public async pickColor(): Promise { let { doc } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.DocumentColor, doc.textDocument) let info = await this.getColorInformation(doc.bufnr) if (!info) return void window.showWarningMessage('Color not found at current position') let { color } = info let colorArr = [(color.red * 255).toFixed(0), (color.green * 255).toFixed(0), (color.blue * 255).toFixed(0)] let res = await this.nvim.call('coc#color#pick_color', [colorArr]) if (!res) return let hex = toHexString({ red: (res[0] / 65535), green: (res[1] / 65535), blue: (res[2] / 65535), alpha: 1 }) await doc.applyEdits([{ range: info.range, newText: `#${hex}` }]) } public isEnabled(bufnr: number): boolean { let highlighter = this.highlighters.getItem(bufnr) return highlighter != null && highlighter.enabled === true } public clearHighlight(bufnr: number): void { let highlighter = this.highlighters.getItem(bufnr) if (highlighter) highlighter.clearHighlight() } public hasColor(bufnr: number): boolean { let highlighter = this.highlighters.getItem(bufnr) if (!highlighter) return false return highlighter.hasColor() } public hasColorAtPosition(bufnr: number, position: Position): boolean { let highlighter = this.highlighters.getItem(bufnr) if (!highlighter) return false return highlighter.hasColorAtPosition(position) } public highlightAll(): void { for (let buf of this.highlighters.items) { buf.highlight() } } public async doHighlight(bufnr: number): Promise { let highlighter = this.highlighters.getItem(bufnr) if (highlighter) await highlighter.doHighlight() } public async getColorInformation(bufnr: number): Promise { let highlighter = this.highlighters.getItem(bufnr) if (!highlighter) return null let position = await window.getCursorPosition() for (let info of highlighter.colors) { let { range } = info let { start, end } = range if (position.line == start.line && position.character >= start.character && position.character <= end.character) { return info } } return null } public dispose(): void { this.highlighters.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/handler/commands.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import commandManager from '../commands' import listManager from '../list/manager' import workspace from '../workspace' import * as Is from '../util/is' function validCommand(command: any): boolean { return command && Is.string(command.id) && Is.string(command.cmd) && command.id.length > 0 && command.cmd.length > 0 } export default class Commands { constructor(private nvim: Neovim) { for (let item of workspace.env.vimCommands) { this.addVimCommand(item) } } public addVimCommand(cmd: { id: string; cmd: string; title?: string }): void { if (!validCommand(cmd)) return let id = `vim.${cmd.id}` commandManager.registerCommand(id, () => { this.nvim.command(cmd.cmd, true) this.nvim.redrawVim() }) if (cmd.title) commandManager.titles.set(id, cmd.title) } public getCommandList(): string[] { return commandManager.commandList.map(o => o.id) } public async repeat(): Promise { await commandManager.repeatCommand() } public async runCommand(id?: string, ...args: any[]): Promise { if (id) return await commandManager.fireCommand(id, ...args) await listManager.start(['commands']) } } ================================================ FILE: src/handler/fold.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import type { FoldingRangeKind } from 'vscode-languageserver-types' import languages, { ProviderName } from '../languages' import { HandlerDelegate } from './types' export default class FoldHandler { constructor(private nvim: Neovim, private handler: HandlerDelegate) { } public async fold(kind?: FoldingRangeKind): Promise { let { doc, winid } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.FoldingRange, doc.textDocument) await doc.synchronize() let win = this.nvim.createWindow(winid) let foldlevel = await this.nvim.eval('&foldlevel') as number let ranges = await this.handler.withRequestToken('foldingrange', token => { return languages.provideFoldingRanges(doc.textDocument, {}, token) }, true) if (!ranges || !ranges.length) return false if (kind) ranges = ranges.filter(o => o.kind == kind) ranges.sort((a, b) => b.startLine - a.startLine) this.nvim.pauseNotification() win.setOption('foldmethod', 'manual', true) this.nvim.command('normal! zE', true) for (let range of ranges) { let { startLine, endLine } = range let cmd = `${startLine + 1}, ${endLine + 1}fold` this.nvim.command(cmd, true) } win.setOption('foldenable', true, true) win.setOption('foldlevel', foldlevel, true) await this.nvim.resumeNotification(true) return true } } ================================================ FILE: src/handler/format.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import commandManager from '../commands' import events from '../events' import languages, { ProviderName } from '../languages' import { createLogger } from '../logger' import Document from '../model/document' import { IConfigurationChangeEvent } from '../types' import { isFalsyOrEmpty } from '../util/array' import { pariedCharacters } from '../util/index' import { isAlphabet } from '../util/string' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' const logger = createLogger('handler-format') interface FormatPreferences { formatOnType: boolean formatOnTypeFiletypes: string[] | null bracketEnterImprove: boolean } export default class FormatHandler { private preferences: FormatPreferences constructor( private nvim: Neovim, private handler: HandlerDelegate ) { this.setConfiguration() handler.addDisposable(workspace.onDidChangeConfiguration(this.setConfiguration, this)) handler.addDisposable(window.onDidChangeActiveTextEditor(() => { this.setConfiguration() })) handler.addDisposable(events.on('Enter', async bufnr => { let res = await events.race(['CursorMovedI'], 100) if (res.args && res.args[0] === bufnr) { await this.handleEnter(bufnr) } })) handler.addDisposable(events.on('TextInsert', async (bufnr: number, _info, character: string) => { let doc = workspace.getDocument(bufnr) if (!events.completing && doc && doc.attached) await this.tryFormatOnType(character, doc) })) handler.addDisposable(commandManager.registerCommand('editor.action.formatDocument', async (uri?: string | number) => { let doc: Document | undefined if (uri) { doc = workspace.getAttachedDocument(uri) } else { let buf = await nvim.buffer doc = workspace.getAttachedDocument(buf.id) } await this.documentFormat(doc) })) commandManager.titles.set('editor.action.formatDocument', 'Format Document') } private setConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('coc.preferences')) { let doc = window.activeTextEditor?.document let config = workspace.getConfiguration('coc.preferences', doc) this.preferences = { formatOnType: config.get('formatOnType', false), formatOnTypeFiletypes: config.get('formatOnTypeFiletypes', null), bracketEnterImprove: config.get('bracketEnterImprove', true), } } } public shouldFormatOnType(filetype: string): boolean { const filetypes = this.preferences.formatOnTypeFiletypes return isFalsyOrEmpty(filetypes) || filetypes.includes(filetype) || filetypes.includes('*') } public async tryFormatOnType(ch: string, doc: Document): Promise { if (doc.getVar('disable_autoformat', 0)) return false if (!this.preferences.formatOnType) return false if (!ch || isAlphabet(ch.charCodeAt(0))) return false if (!this.shouldFormatOnType(doc.filetype)) return false if (!languages.hasProvider(ProviderName.FormatOnType, doc.textDocument)) { logger.warn(`Format on type provider not found for buffer: ${doc.uri}`) return false } if (!languages.canFormatOnType(ch, doc.textDocument)) return false let position: Position let edits = await this.handler.withRequestToken('Format on type', async token => { position = await window.getCursorPosition() await doc.synchronize() return await languages.provideDocumentOnTypeEdits(ch, doc.textDocument, position, token) }) if (edits == null || events.completing) return false if (edits.length === 0) return true await doc.applyEdits(edits, false, true) this.logProvider(doc.bufnr, edits) return true } public async formatCurrentBuffer(): Promise { let { doc } = await this.handler.getCurrentState() return await this.documentFormat(doc) } public async formatCurrentRange(mode: string): Promise { let { doc } = await this.handler.getCurrentState() return await this.documentRangeFormat(doc, mode) } public async documentFormat(doc: Document): Promise { await doc.synchronize() if (!languages.hasFormatProvider(doc.textDocument)) { throw new Error(`Format provider not found for buffer: ${doc.bufnr}`) } let options = await workspace.getFormatOptions(doc.uri) let textEdits = await this.handler.withRequestToken('format', token => { return languages.provideDocumentFormattingEdits(doc.textDocument, options, token) }) if (textEdits && textEdits.length > 0) { await doc.applyEdits(textEdits, false, true) this.logProvider(doc.bufnr, textEdits) return true } return false } public async handleEnter(bufnr: number): Promise { let { nvim } = this let { bracketEnterImprove } = this.preferences let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) return await this.tryFormatOnType('\n', doc) if (bracketEnterImprove) { let line = (await nvim.call('line', '.') as number) - 1 await doc.patchChange() let pre = doc.getline(line - 1) let curr = doc.getline(line) let firstLine = doc.getline(0) let prevChar = pre[pre.length - 1] if (prevChar && pariedCharacters.has(prevChar)) { let nextChar = curr.trim()[0] if (nextChar && pariedCharacters.get(prevChar) == nextChar) { let edits: TextEdit[] = [] let pos: Position = Position.create(line - 1, pre.length) let opts = await workspace.getFormatOptions(doc.uri) let space = opts.insertSpaces ? ' '.repeat(opts.tabSize) : '\t' let preIndent = pre.match(/^\s*/)[0] let currIndent = curr.match(/^\s*/)[0] let newText = '\n' + preIndent + space // vim legacy script needs continuation markers if (doc.filetype == 'vim' && !firstLine.startsWith('vim9script')) { edits.push({ range: Range.create(line, currIndent.length, line, currIndent.length), newText: ' \\ ' }) newText = '\n' + currIndent + space + '\\ ' } edits.push({ range: Range.create(pos, pos), newText }) await doc.applyEdits(edits, true) await window.moveTo(Position.create(line, newText.length - 1)) } } } } public logProvider(bufnr: number, edits: TextEdit[] | undefined): void { if (!Array.isArray(edits) || edits.length === 0) return let extensionName = edits['__extensionName'] if (extensionName) logger.info(`Format buffer ${bufnr} by ${extensionName}`) } public async documentRangeFormat(doc: Document, mode?: string): Promise { this.handler.checkProvider(ProviderName.FormatRange, doc.textDocument) await doc.synchronize() let range: Range if (mode) { range = await window.getSelectedRange(mode) if (!range) return -1 } else { let [lnum, count, mode] = await this.nvim.eval("[v:lnum,v:count,mode()]") as [number, number, string] // we can't handle if (count == 0 || mode == 'i' || mode == 'R') return -1 range = Range.create(lnum - 1, 0, lnum - 1 + count, 0) } let options = await workspace.getFormatOptions(doc.uri) let textEdits = await this.handler.withRequestToken('Format range', token => { return languages.provideDocumentRangeFormattingEdits(doc.textDocument, range, options, token) }) if (!isFalsyOrEmpty(textEdits)) { await doc.applyEdits(textEdits, false, true) this.logProvider(doc.bufnr, textEdits) return 0 } return -1 } } ================================================ FILE: src/handler/highlights.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { DocumentHighlight, DocumentHighlightKind, Position, Range } from 'vscode-languageserver-types' import commands from '../commands' import events from '../events' import languages, { ProviderName } from '../languages' import Document from '../model/document' import { IConfigurationChangeEvent } from '../types' import { disposeAll } from '../util' import { comparePosition, compareRangesUsingStarts } from '../util/position' import { CancellationTokenSource, Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' interface HighlightConfig { limit: number priority: number timeout: number } /** * Highlight same symbols on current window. * Highlights are added to window by matchaddpos. */ export default class Highlights { private config: HighlightConfig private disposables: Disposable[] = [] private tokenSource: CancellationTokenSource private highlights: Map = new Map() private timer: NodeJS.Timeout constructor(private nvim: Neovim, private handler: HandlerDelegate) { events.on(['CursorMoved', 'CursorMovedI'], () => { this.cancel() this.clearHighlights() }, null, this.disposables) this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) window.onDidChangeActiveTextEditor(() => { this.loadConfiguration() }, null, this.disposables) commands.register({ id: 'document.jumpToNextSymbol', execute: async () => { await this.jumpSymbol('next') } }, false, 'Jump to next symbol highlight position.') commands.register({ id: 'document.jumpToPrevSymbol', execute: async () => { await this.jumpSymbol('previous') } }, false, 'Jump to previous symbol highlight position.') } private loadConfiguration(e?: IConfigurationChangeEvent): void { let config = workspace.getConfiguration('documentHighlight', this.handler.uri) if (!e || e.affectsConfiguration('documentHighlight')) { this.config = Object.assign(this.config || {}, { limit: config.get('limit', 200), priority: config.get('priority', -1), timeout: config.get('timeout', 300) }) } } public isEnabled(bufnr: number, cursors: number): boolean { let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached || cursors) return false if (!languages.hasProvider(ProviderName.DocumentHighlight, doc.textDocument)) return false return true } public clearHighlights(): void { if (this.highlights.size == 0) return for (let winid of this.highlights.keys()) { let win = this.nvim.createWindow(winid) win.clearMatchGroup('^CocHighlight') } this.highlights.clear() } public async highlight(): Promise { let { nvim } = this this.cancel() let [bufnr, winid, pos, cursors] = await nvim.eval(`[bufnr("%"),win_getid(),coc#cursor#position(),get(b:,'coc_cursors_activated',0)]`) as [number, number, [number, number], number] if (!this.isEnabled(bufnr, cursors)) return let doc = workspace.getDocument(bufnr) let highlights = await this.getHighlights(doc, Position.create(pos[0], pos[1])) if (!highlights) return let groups: { [index: string]: Range[] } = {} for (let hl of highlights) { if (!Range.is(hl.range)) continue let hlGroup = hl.kind == DocumentHighlightKind.Text ? 'CocHighlightText' : hl.kind == DocumentHighlightKind.Read ? 'CocHighlightRead' : 'CocHighlightWrite' groups[hlGroup] = groups[hlGroup] || [] groups[hlGroup].push(hl.range) } let win = nvim.createWindow(winid) nvim.pauseNotification() win.clearMatchGroup('^CocHighlight') for (let [hlGroup, ranges] of Object.entries(groups)) { win.highlightRanges(hlGroup, ranges, -1, true) } nvim.resumeNotification(true, true) this.highlights.set(winid, highlights) } public async jumpSymbol(direction: 'previous' | 'next'): Promise { let ranges = await this.getSymbolsRanges() if (!ranges) return let pos = await window.getCursorPosition() if (direction == 'next') { for (let i = 0; i <= ranges.length - 1; i++) { if (comparePosition(ranges[i].start, pos) > 0) { await window.moveTo(ranges[i].start) return } } await window.moveTo(ranges[0].start) } else { for (let i = ranges.length - 1; i >= 0; i--) { if (comparePosition(ranges[i].end, pos) < 0) { await window.moveTo(ranges[i].start) return } } await window.moveTo(ranges[ranges.length - 1].start) } } public async getSymbolsRanges(): Promise { let { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.DocumentHighlight, doc.textDocument) let highlights = await this.getHighlights(doc, position) if (!highlights) return null return highlights.filter(o => Range.is(o.range)).map(o => o.range).sort((a, b) => { return compareRangesUsingStarts(a, b) }) } public hasHighlights(winid: number): boolean { return this.highlights.get(winid) != null } public async getHighlights(doc: Document, position: Position): Promise { let line = doc.getline(position.line) let ch = line[position.character] if (!ch || !doc.isWord(ch)) return null await doc.synchronize() this.cancel() let source = this.tokenSource = new CancellationTokenSource() let timer = this.timer = setTimeout(() => { source.cancel() }, this.config.timeout) let highlights = await languages.getDocumentHighLight(doc.textDocument, position, source.token) clearTimeout(timer) if (source.token.isCancellationRequested) return null return highlights } private cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource.dispose() this.tokenSource = null } } public dispose(): void { if (this.timer) clearTimeout(this.timer) this.cancel() this.highlights.clear() disposeAll(this.disposables) } } ================================================ FILE: src/handler/hover.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { DefinitionLink, Hover, MarkedString, MarkupContent, Position, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { IConfigurationChangeEvent } from '../configuration/types' import languages, { ProviderName } from '../languages' import Document from '../model/document' import { TextDocumentContentProvider } from '../provider' import { Documentation, FloatConfig, FloatFactory, HoverTarget } from '../types' import { disposeAll, getConditionValue } from '../util' import { isFalsyOrEmpty } from '../util/array' import { readFileLines } from '../util/fs' import { isMarkdown } from '../util/is' import { fs } from '../util/node' import { CancellationTokenSource, Disposable } from '../util/protocol' import { characterIndex } from '../util/string' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' interface HoverConfig { target: HoverTarget floatConfig: FloatConfig previewMaxHeight: number autoHide: boolean } interface HoverLocation { bufnr?: number line: number col: number } const highlightDelay = getConditionValue(500, 10) export default class HoverHandler { private hoverFactory: FloatFactory private disposables: Disposable[] = [] private documentLines: string[] = [] private config: HoverConfig private timer: NodeJS.Timeout private hasProvider = false constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) this.hoverFactory = window.createFloatFactory({ modes: ['n'], autoHide: this.config.autoHide }) this.disposables.push(this.hoverFactory) window.onDidChangeActiveTextEditor(() => { this.loadConfiguration() }, null, this.disposables) } private registerProvider(): void { if (this.hasProvider) return this.hasProvider = true let { nvim } = this let provider: TextDocumentContentProvider = { onDidChange: null, provideTextDocumentContent: async () => { nvim.pauseNotification() nvim.command('setlocal conceallevel=2 nospell nofoldenable wrap', true) nvim.command('setlocal bufhidden=wipe nobuflisted', true) nvim.command('setfiletype markdown', true) nvim.command(`if winnr('j') != winnr('k') | exe "normal! z${Math.min(this.documentLines.length, this.config.previewMaxHeight)}\\" | endif`, true) await nvim.resumeNotification() return this.documentLines.join('\n') } } this.disposables.push(workspace.registerTextDocumentContentProvider('coc', provider)) } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('hover')) { let config = workspace.getConfiguration('hover', this.handler.uri) this.config = { floatConfig: config.get('floatConfig', {}), autoHide: config.get('autoHide', true), target: config.get('target', 'float'), previewMaxHeight: config.get('previewMaxHeight', 12) } if (this.config.target == 'preview') { this.registerProvider() } } } public async onHover(hoverTarget?: HoverTarget): Promise { let { doc, position, winid } = await this.handler.getCurrentState() if (hoverTarget == 'preview') this.registerProvider() this.handler.checkProvider(ProviderName.Hover, doc.textDocument) await doc.synchronize() let hovers = await this.handler.withRequestToken('hover', token => { return languages.getHover(doc.textDocument, position, token) }, true) if (hovers == null || !hovers.length) return false let hover = hovers.find(o => Range.is(o.range)) if (hover?.range) { let win = this.nvim.createWindow(winid) win.highlightRanges('CocHoverRange', [hover.range], 1024, true) this.timer = setTimeout(() => { win.clearMatchGroup('CocHoverRange') this.nvim.redrawVim() }, 500) } await this.previewHover(hovers, hoverTarget) return true } public async definitionHover(hoverTarget: HoverTarget): Promise { const { doc, position, winid } = await this.handler.getCurrentState() if (hoverTarget == 'preview') this.registerProvider() this.handler.checkProvider(ProviderName.Hover, doc.textDocument) await doc.synchronize() const hovers: (Hover | Documentation)[] = await this.handler.withRequestToken('hover', token => { return languages.getHover(doc.textDocument, position, token) }, true) if (isFalsyOrEmpty(hovers)) return false const defs = await this.handler.withRequestToken('definitionHover', token => { return languages.getDefinitionLinks(doc.textDocument, position, token) }, false) // could be cancelled if (defs == null) return false await addDefinitions(hovers, defs, doc.filetype) let hover = hovers.find(o => Hover.is(o) && Range.is(o.range)) as Hover | undefined if (hover?.range) { let win = this.nvim.createWindow(winid) win.highlightRanges('CocHoverRange', [hover.range], 1024, true) this.timer = setTimeout(() => { win.clearMatchGroup('CocHoverRange') this.nvim.redrawVim() }, highlightDelay) } await this.previewHover(hovers, hoverTarget) return true } private async previewHover(hovers: (Hover | Documentation)[], target?: string): Promise { let docs: Documentation[] = [] target = target ?? this.config.target let isPreview = target === 'preview' for (let hover of hovers) { if (isDocumentation(hover)) { docs.push(hover) continue } let { contents } = hover if (Array.isArray(contents)) { for (let item of contents) { if (typeof item === 'string') { addDocument(docs, item, 'markdown', isPreview) } else { addDocument(docs, item.value, item.language, isPreview) } } } else if (MarkedString.is(contents)) { if (typeof contents == 'string') { addDocument(docs, contents, 'markdown', isPreview) } else { addDocument(docs, contents.value, contents.language, isPreview) } } else if (MarkupContent.is(contents)) { addDocument(docs, contents.value, isMarkdown(contents) ? 'markdown' : 'txt', isPreview) } } if (target == 'float') { await this.hoverFactory.show(docs, this.config.floatConfig) return } let lines = docs.reduce((p, c) => { let arr = c.content.split(/\r?\n/) if (p.length > 0) p.push('') p.push(...arr) return p }, []) if (target == 'echo') { const msg = lines.join('\n').trim() await this.nvim.call('coc#ui#echo_hover', [msg]) } else { this.documentLines = lines await this.nvim.command(`noswapfile pedit coc://document`) } } /** * Get hover text array */ public async getHover(loc?: HoverLocation): Promise { let result: string[] = [] let doc: Document let position: Position if (!loc) { let state = await this.handler.getCurrentState() doc = state.doc position = state.position } else { doc = loc.bufnr ? workspace.getAttachedDocument(loc.bufnr) : await workspace.document let line = doc.getline(loc.line - 1) let character = characterIndex(line, loc.col - 1) position = Position.create(loc.line - 1, character) } this.handler.checkProvider(ProviderName.Hover, doc.textDocument) await doc.synchronize() let tokenSource = new CancellationTokenSource() let hovers = await languages.getHover(doc.textDocument, position, tokenSource.token) for (let h of hovers) { let { contents } = h if (Array.isArray(contents)) { contents.forEach(c => { result.push(typeof c === 'string' ? c : c.value) }) } else if (MarkupContent.is(contents)) { result.push(contents.value) } else { result.push(typeof contents === 'string' ? contents : contents.value) } } result = result.filter(s => s != null && s.length > 0) return result } public dispose(): void { if (this.timer) clearTimeout(this.timer) disposeAll(this.disposables) } } export async function addDefinitions(hovers: (Hover | Documentation)[], definitions: DefinitionLink[], filetype: string): Promise { for (const def of definitions) { if (!def?.targetRange) continue const { start, end } = def.targetRange // def.targetSelectionRange const endLine = end.line - start.line >= 100 ? start.line + 100 : (end.character == 0 ? end.line - 1 : end.line) let lines = await readLines(def.targetUri, start.line, endLine) if (lines.length) { let indent = lines[0].match(/^\s*/)[0] if (indent) lines = lines.map(l => l.startsWith(indent) ? l.substring(indent.length) : l) hovers.push({ content: lines.join('\n'), filetype }) } } } export function addDocument(docs: Documentation[], text: string, filetype: string, isPreview = false): void { let content = text.trim() if (!content.length) return if (isPreview && filetype !== 'markdown') { content = '``` ' + filetype + '\n' + content + '\n```' } docs.push({ content, filetype }) } export function isDocumentation(obj: any): obj is Documentation { if (!obj) return false return typeof obj.filetype === 'string' && typeof obj.content === 'string' } export async function readLines(uri: string, start: number, end: number): Promise { let doc = workspace.getDocument(uri) if (doc) return doc.getLines(start, end + 1) let fsPath = URI.parse(uri).fsPath if (!fs.existsSync(fsPath)) return [] return await readFileLines(fsPath, start, end) } ================================================ FILE: src/handler/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { CodeAction, CodeActionKind, Location, Position, Range, SymbolKind } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import events from '../events' import languages, { ProviderName } from '../languages' import { createLogger } from '../logger' import Document from '../model/document' import { StatusBarItem } from '../model/status' import { TextDocumentMatch } from '../types' import { disposeAll, getConditionValue } from '../util' import { getSymbolKind } from '../util/convert' import * as Is from '../util/is' import { hasOwnProperty, toObject } from '../util/object' import { CancellationToken, CancellationTokenSource, Disposable } from '../util/protocol' import { getRangesFromEdit } from '../util/textedit' import window from '../window' import workspace from '../workspace' import CallHierarchy from './callHierarchy' import CodeActions from './codeActions' import CodeLens from './codelens/index' import Colors from './colors/index' import Commands from './commands' import Fold from './fold' import Format from './format' import Highlights from './highlights' import HoverHandler from './hover' import InlayHintHandler from './inlayHint/index' import InlineCompletion, { InlineSuggestOption } from './inline' import LinkedEditingHandler from './linkedEditing' import Links from './links' import Locations from './locations' import Refactor from './refactor/index' import Rename from './rename' import SelectionRange from './selectionRange' import SemanticTokens from './semanticTokens/index' import Signature from './signature' import Symbols from './symbols/index' import TypeHierarchy from './typeHierarchy' import { HandlerDelegate } from './types' import WorkspaceHandler from './workspace' const logger = createLogger('Handler') const requestTimeout = getConditionValue(500, 10) export interface CurrentState { doc: Document winid: number position: Position // :h mode() mode: string } export default class Handler implements HandlerDelegate { public readonly documentHighlighter: Highlights public readonly colors: Colors public readonly signature: Signature public readonly locations: Locations public readonly symbols: Symbols public readonly refactor: Refactor public readonly codeActions: CodeActions public readonly format: Format public readonly hover: HoverHandler public readonly codeLens: CodeLens public readonly commands: Commands public readonly links: Links public readonly rename: Rename public readonly fold: Fold public readonly selectionRange: SelectionRange public readonly callHierarchy: CallHierarchy public readonly typeHierarchy: TypeHierarchy public readonly inlineCompletion: InlineCompletion public readonly semanticHighlighter: SemanticTokens public readonly workspace: WorkspaceHandler public readonly linkedEditingHandler: LinkedEditingHandler public readonly inlayHintHandler: InlayHintHandler private _requestStatusItem: StatusBarItem private requestTokenSource: CancellationTokenSource | undefined private requestTimer: NodeJS.Timeout private disposables: Disposable[] = [] constructor(private nvim: Neovim) { events.on(['CursorMoved', 'CursorMovedI', 'InsertEnter', 'InsertSnippet', 'InsertLeave'], () => { if (this.requestTokenSource) { this.requestTokenSource.cancel() this.requestTokenSource = null } }, null, this.disposables) this.fold = new Fold(nvim, this) this.links = new Links(nvim, this) this.codeLens = new CodeLens(nvim) this.colors = new Colors(nvim, this) this.format = new Format(nvim, this) this.symbols = new Symbols(nvim, this) this.refactor = new Refactor(nvim, this) this.hover = new HoverHandler(nvim, this) this.locations = new Locations(nvim, this) this.signature = new Signature(nvim, this) this.rename = new Rename(nvim, this) this.workspace = new WorkspaceHandler(nvim) this.codeActions = new CodeActions(nvim, this) this.commands = new Commands(nvim) this.callHierarchy = new CallHierarchy(nvim, this) this.typeHierarchy = new TypeHierarchy(nvim, this) this.documentHighlighter = new Highlights(nvim, this) this.semanticHighlighter = new SemanticTokens(nvim) this.selectionRange = new SelectionRange(nvim, this) this.linkedEditingHandler = new LinkedEditingHandler(nvim, this) this.inlayHintHandler = new InlayHintHandler(nvim, this) this.inlineCompletion = new InlineCompletion(nvim, this) this.disposables.push({ dispose: () => { this.callHierarchy.dispose() this.typeHierarchy.dispose() this.codeLens.dispose() this.links.dispose() this.refactor.dispose() this.signature.dispose() this.symbols.dispose() this.hover.dispose() this.colors.dispose() this.documentHighlighter.dispose() this.semanticHighlighter.dispose() this.inlineCompletion.dispose() } }) this.registerCommands() } private registerCommands(): void { commands.register({ id: 'document.renameCurrentWord', execute: async () => { let doc = await workspace.document let edit = await this.rename.getWordEdit() let ranges = getRangesFromEdit(doc.uri, toObject(edit)) if (!ranges) return window.showWarningMessage('Invalid position') await commands.executeCommand('editor.action.addRanges', ranges) } }, false, 'rename word under cursor in current buffer by multiple cursors.') commands.register({ id: ['workbench.action.reloadWindow', 'editor.action.restart'], execute: () => { this.nvim.command('CocRestart', true) } }, true) commands.register({ id: 'workbench.action.openSettingsJson', execute: () => { this.nvim.command('CocConfig', true) } }, true) this.register('vscode.open', async (url: string | URI) => { await workspace.openResource(url.toString()) }) this.register('editor.action.doCodeAction', async (action: CodeAction) => { await this.codeActions.applyCodeAction(action) }) this.register('editor.action.triggerParameterHints', async () => { await this.signature.triggerSignatureHelp() }) this.register('editor.action.showReferences', async (uri: string | URI, position: Position, references: Location[]) => { await workspace.jumpTo(uri, position) await workspace.showLocations(references) }) this.register('editor.action.rename', async (uri: string | URI | [URI, Position], position: Position, newName?: string) => { if (Array.isArray(uri)) { position = uri[1] uri = uri[0] } await workspace.jumpTo(uri, position) return await this.rename.rename(newName) }) this.register('editor.action.format', async () => { await this.format.formatCurrentBuffer() }) this.register('editor.action.showRefactor', async (locations: Location[]) => { let locs = locations.filter(o => Location.is(o)) return await this.refactor.fromLocations(locs) }) this.register('editor.action.triggerInlineCompletion', async (option?: InlineSuggestOption) => { let bufnr = await this.nvim.eval('bufnr("%")') as number return await this.inlineCompletion.trigger(bufnr, option) }) } private register(key, handler: (...args: any[]) => T | Promise): void { this.disposables.push(commands.registerCommand(key, handler, null, true)) } private get requestStatusItem(): StatusBarItem { if (this._requestStatusItem) return this._requestStatusItem this._requestStatusItem = window.createStatusBarItem(0, { progress: true }) return this._requestStatusItem } private get labels(): { [key: string]: string } { let configuration = workspace.initialConfiguration return configuration.get('suggest.completionItemKindLabels', {}) } public get uri(): string | undefined { return window.activeTextEditor?.uri } public async getCurrentState(): Promise { let { nvim } = this let [bufnr, [line, character], winid, mode] = await nvim.eval("[bufnr('%'),coc#cursor#position(),win_getid(),mode()]") as [number, [number, number], number, string] let doc = workspace.getAttachedDocument(bufnr) return { doc, mode, position: Position.create(line, character), winid } } public addDisposable(disposable: Disposable): void { this.disposables.push(disposable) } /** * Throw error when provider doesn't exist. */ public checkProvider(id: ProviderName, document: TextDocumentMatch): void { if (!languages.hasProvider(id, document)) { throw new Error(`${id} provider not found for current buffer, your language server doesn't support it.`) } } public async withRequestToken(name: string, fn: (token: CancellationToken) => Thenable, checkEmpty?: boolean): Promise { if (this.requestTokenSource) { this.requestTokenSource.cancel() this.requestTokenSource.dispose() } clearTimeout(this.requestTimer) let statusItem = this.requestStatusItem this.requestTokenSource = new CancellationTokenSource() let { token } = this.requestTokenSource token.onCancellationRequested(() => { statusItem.text = `${name} request canceled` statusItem.isProgress = false this.requestTimer = setTimeout(() => { statusItem.hide() }, requestTimeout) }) statusItem.isProgress = true statusItem.text = `requesting ${name}` statusItem.show() let res: T try { res = await Promise.resolve(fn(token)) } catch (e) { logger.error(`Error on request ${name}`, e) this.nvim.errWriteLine(`Error on ${name}: ${e}`) } if (this.requestTokenSource) { this.requestTokenSource.dispose() this.requestTokenSource = undefined } if (token.isCancellationRequested) return null statusItem.hide() if (checkEmpty && (!res || (Array.isArray(res) && res.length == 0))) { void window.showWarningMessage(`${name} not found`) return null } return res } public getIcon(kind: SymbolKind): { text: string, hlGroup: string } { let { labels } = this let kindText = getSymbolKind(kind) const key = kindText[0].toLowerCase() + kindText.slice(1) let text = hasOwnProperty(labels, key) ? labels[key] : undefined if (!Is.string(text) || !text.length) text = Is.string(labels['default']) ? labels['default'] : kindText[0].toLowerCase() return { text, hlGroup: kindText == 'Unknown' ? 'CocSymbolDefault' : `CocSymbol${kindText}` } } public async getCodeActions(doc: Document, range?: Range, only?: CodeActionKind[]): Promise { let codeActions = await this.codeActions.getCodeActions(doc, range, only) return codeActions.filter(o => !o.disabled) } public async applyCodeAction(action: CodeAction): Promise { await this.codeActions.applyCodeAction(action) } public async hasProvider(id: string, bufnr?: number): Promise { if (!bufnr) bufnr = await this.nvim.call('bufnr', '%') as number let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) return false return languages.hasProvider(id as ProviderName, doc.textDocument) } public dispose(): void { if (this.requestTimer) { clearTimeout(this.requestTimer) } disposeAll(this.disposables) } } ================================================ FILE: src/handler/inlayHint/buffer.ts ================================================ 'use strict' import { Neovim, VirtualTextOption } from '@chemzqm/neovim' import { InlayHintKind, Range } from 'vscode-languageserver-types' import events from '../../events' import languages, { ProviderName } from '../../languages' import { createLogger } from '../../logger' import { SyncItem } from '../../model/bufferSync' import Document from '../../model/document' import Regions from '../../model/regions' import { getLabel, InlayHintWithProvider } from '../../provider/inlayHintManager' import { getConditionValue, waitWithToken } from '../../util' import { CancellationError, onUnexpectedError } from '../../util/errors' import { positionInRange } from '../../util/position' import { CancellationToken, CancellationTokenSource, Emitter, Event } from '../../util/protocol' import { byteIndex } from '../../util/string' import window from '../../window' import workspace from '../../workspace' const logger = createLogger('inlayHint-buffer') export interface InlayHintConfig { enable: boolean position: InlayHintPosition, display: boolean filetypes: string[] refreshOnInsertMode: boolean enableParameter: boolean maximumLength: number } export interface VirtualTextItem extends VirtualTextOption { /** * Zero based line number */ line: number /** * List with [text, hl_group] */ blocks: [string, string][] } export enum InlayHintPosition { Inline = "inline", Eol = "eol", } export interface RenderConfig { winid: number region: [number, number] } let srcId: number | undefined const debounceInterval = getConditionValue(150, 10) const requestDelay = getConditionValue(500, 10) function getHighlightGroup(kind: InlayHintKind): string { switch (kind) { case InlayHintKind.Parameter: return 'CocInlayHintParameter' case InlayHintKind.Type: return 'CocInlayHintType' default: return 'CocInlayHint' } } /** * Full Virtual text render when first render. * Update visible regions on TextChange and visible lines change. */ export default class InlayHintBuffer implements SyncItem { private tokenSource: CancellationTokenSource private regions = new Regions() private _config: InlayHintConfig | undefined private _dirty = false private _changedtick: number // Saved for resolve and TextEdits in the future. private currentHints: InlayHintWithProvider[] = [] private readonly _onDidRefresh = new Emitter() public readonly onDidRefresh: Event = this._onDidRefresh.event constructor( private readonly nvim: Neovim, public readonly doc: Document ) { this.render().catch(onUnexpectedError) } public get config(): InlayHintConfig { if (this._config) return this._config this.loadConfiguration() return this._config } public loadConfiguration(): void { let config = workspace.getConfiguration('inlayHint', this.doc) let changeEnable = this._config && this._config.enable !== config.enable let changeDisplay = this._config && this._config.display !== config.display this._config = { enable: config.get('enable'), position: config.get('position'), display: config.get('display', true), filetypes: config.get('filetypes'), refreshOnInsertMode: config.get('refreshOnInsertMode'), enableParameter: config.get('enableParameter'), maximumLength: config.get('maximumLength', 0), } if (changeEnable || changeDisplay) { let { enable, display } = this._config if (enable && display) { this.render(undefined, 0).catch(onUnexpectedError) } else { this.clearCache() this.clearVirtualText() } } } public onInsertLeave(): void { if (this.config.refreshOnInsertMode || this.doc.changedtick === this._changedtick) return this.render().catch(onUnexpectedError) } public onInsertEnter(): void { this._changedtick = this.doc.changedtick if (this.config.refreshOnInsertMode) return this.cancel() } public get current(): ReadonlyArray { return this.currentHints } public get enabled(): boolean { if (!this.config.display || !this.configEnabled) return false return this.hasProvider } private get hasProvider(): boolean { return languages.hasProvider(ProviderName.InlayHint, this.doc) } public get configEnabled(): boolean { let { filetypes, enable } = this.config if (Array.isArray(filetypes)) return filetypes.includes('*') || filetypes.includes(this.doc.filetype) return enable === true } public enable() { this.checkState() this.config.display = true this.render(undefined, 0).catch(onUnexpectedError) } public disable() { this.checkState() this.config.display = false this.clearCache() this.clearVirtualText() } private checkState(): void { if (!languages.hasProvider(ProviderName.InlayHint, this.doc.textDocument)) throw new Error('Inlay hint provider not found for current document') if (!this.configEnabled) throw new Error(`Filetype "${this.doc.filetype}" not enabled by inlayHint configuration, see ':h coc-config-inlayHint'`) } public toggle(): void { if (this.config.display) { this.disable() } else { this.enable() } } public clearCache(): void { this.cancel() this.currentHints = [] this.regions.clear() } public onTextChange(): void { this.clearCache() } public onChange(): void { this.render().catch(onUnexpectedError) } public cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } public onVisible(winid: number, region: Readonly<[number, number]>): void { // Ensure rendered once before range render. if (!this._dirty) return // already debounced this.render({ winid, region: [region[0], region[1]] }, 0).catch(onUnexpectedError) } public async render(config?: RenderConfig, delay?: number): Promise { if (!this.enabled) return if (!this.config.refreshOnInsertMode && events.bufnr === this.doc.bufnr && events.insertMode) return this.cancel() this.tokenSource = new CancellationTokenSource() let token = this.tokenSource.token await waitWithToken(typeof delay === 'number' ? delay : debounceInterval, token) if (!srcId) srcId = await this.nvim.createNamespace('coc-inlayHint') if (token.isCancellationRequested || this.doc.dirty) return if (!this._dirty) { await this.renderAll(token) } else if (config) { let region = config.region await this.renderRange([region[0] - 1, region[1] - 1], token) } else { // Could be text change or provider change. const spans = await window.getVisibleRanges(this.doc.bufnr) for (const [topline, botline] of spans) { if (token.isCancellationRequested) break await this.renderRange([topline - 1, botline - 1], token) } } } private async renderAll(token: CancellationToken): Promise { const lineCount = this.doc.lineCount const range = Range.create(0, 0, lineCount, 0) const inlayHints = await this.request(range, token) if (!inlayHints) return this.currentHints = inlayHints this.setVirtualText(range, inlayHints) this.regions.add(0, lineCount) this._dirty = true } /** * 0 based startLine and endLine */ public async renderRange(lines: [number, number], token: CancellationToken): Promise { let span = this.regions.toUncoveredSpan(lines, workspace.env.lines, this.doc.lineCount) if (!span) return const [startLine, endLine] = span const range = this.doc.textDocument.intersectWith(Range.create(startLine, 0, endLine + 1, 0)) const inlayHints = await this.request(range, token) if (!inlayHints) return this.currentHints = this.currentHints.filter(o => positionInRange(o.position, range) !== 0) this.currentHints.push(...inlayHints) this.setVirtualText(range, inlayHints) this.regions.add(startLine, endLine) } private async request(range: Range, token: CancellationToken): Promise { let inlayHints: InlayHintWithProvider[] try { inlayHints = await languages.provideInlayHints(this.doc.textDocument, range, token) } catch (e) { if (!token.isCancellationRequested && e instanceof CancellationError) { // server cancel, wait for more time this.render(undefined, requestDelay).catch(onUnexpectedError) return } } if (inlayHints == null || token.isCancellationRequested) return if (!this.config.enableParameter) { inlayHints = inlayHints.filter(o => o.kind !== InlayHintKind.Parameter) } return inlayHints } public setVirtualText(range: Range, inlayHints: InlayHintWithProvider[]): void { let { nvim, doc } = this let buffer = doc.buffer const { maximumLength } = this.config nvim.pauseNotification() const end = range.end.line >= doc.lineCount ? -1 : range.end.line + 1 buffer.clearNamespace(srcId, range.start.line, end) let lineInfo = { lineNum: 0, totalLineLen: 0 } const vitems: VirtualTextItem[] = [] for (const item of inlayHints) { const blocks = [] let { position } = item if (lineInfo.lineNum !== position.line) { lineInfo = { lineNum: position.line, totalLineLen: 0 } } if (maximumLength > 0 && lineInfo.totalLineLen > maximumLength) { logger.warn(`Inlay hint of ${lineInfo.lineNum} too long, max length: ${maximumLength}, current line total length: ${lineInfo.totalLineLen}`) continue } let line = this.doc.getline(position.line) let col = byteIndex(line, position.character) + 1 let label = getLabel(item) lineInfo.totalLineLen += label.length const over = maximumLength > 0 ? lineInfo.totalLineLen - maximumLength : 0 if (over > 0) { label = label.slice(0, -over) + '…' } if (item.paddingLeft) blocks.push([' ', 'Normal']) blocks.push([label, getHighlightGroup(item.kind)]) if (item.paddingRight) blocks.push([' ', 'Normal']) if (this.config.position == InlayHintPosition.Eol) { col = 0 } let opts: VirtualTextItem = { line: position.line, blocks, col, hl_mode: 'replace' } if (item.kind == InlayHintKind.Parameter) { opts.right_gravity = false } vitems.push(opts) } nvim.call('coc#vtext#set', [buffer.id, srcId, vitems, false, 200], true) nvim.resumeNotification(true, true) this._onDidRefresh.fire() } public clearVirtualText(): void { if (srcId) this.doc.buffer.clearNamespace(srcId) } public dispose(): void { this.clearCache() } } ================================================ FILE: src/handler/inlayHint/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import commands from '../../commands' import events from '../../events' import languages, { ProviderName } from '../../languages' import BufferSync from '../../model/bufferSync' import { disposeAll } from '../../util' import { Disposable } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' import { HandlerDelegate } from '../types' import InlayHintBuffer from './buffer' export type StateMethods = 'enable' | 'disable' | 'toggle' export default class InlayHintHandler { private buffers: BufferSync | undefined private disposables: Disposable[] = [] constructor(nvim: Neovim, handler: HandlerDelegate) { this.buffers = workspace.registerBufferSync(doc => { return new InlayHintBuffer(nvim, doc) }) this.disposables.push(this.buffers) workspace.onDidChangeConfiguration(e => { for (let item of this.buffers.items) { if (e.affectsConfiguration('inlayHint', item.doc)) { item.loadConfiguration() } } }, null, this.disposables) languages.onDidInlayHintRefresh(async e => { for (let item of this.buffers.items) { if (workspace.match(e, item.doc.textDocument)) { item.clearCache() if (languages.hasProvider(ProviderName.InlayHint, item.doc.textDocument)) { await item.render() } else { item.clearVirtualText() } } } }, null, this.disposables) events.on('InsertLeave', bufnr => { let item = this.buffers.getItem(bufnr) if (item) item.onInsertLeave() }, null, this.disposables) events.on('InsertEnter', bufnr => { let item = this.buffers.getItem(bufnr) if (item) item.onInsertEnter() }, null, this.disposables) commands.register({ id: 'document.toggleInlayHint', execute: (bufnr?: number) => { this.setState('toggle', bufnr) }, }, false, 'Toggle inlayHint display of current buffer') commands.register({ id: 'document.enableInlayHint', execute: (bufnr?: number) => { this.setState('enable', bufnr) }, }, false, 'Enable inlayHint display of current buffer') commands.register({ id: 'document.disableInlayHint', execute: (bufnr?: number) => { this.setState('disable', bufnr) }, }, false, 'Disable inlayHint display of current buffer') handler.addDisposable(Disposable.create(() => { disposeAll(this.disposables) })) } public setState(method: StateMethods, bufnr?: number): void { try { bufnr = bufnr ?? workspace.bufnr workspace.getAttachedDocument(bufnr) let item = this.getItem(bufnr) item[method]() } catch (e) { void window.showErrorMessage((e as Error).message) } } public getItem(bufnr: number): InlayHintBuffer { return this.buffers.getItem(bufnr) } } ================================================ FILE: src/handler/inline.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { FormattingOptions, InlineCompletionTriggerKind, Position, Range, StringValue, TextEdit } from 'vscode-languageserver-types' import commands from '../commands' import completion from '../completion' import { IConfigurationChangeEvent } from '../configuration/types' import events from '../events' import languages, { ProviderName } from '../languages' import { createLogger } from '../logger' import Document from '../model/document' import { SnippetParser } from '../snippets/parser' import { defaultValue, disposeAll, waitWithToken } from '../util' import { onUnexpectedError } from '../util/errors' import { comparePosition, emptyRange, getEnd, positionInRange } from '../util/position' import { CancellationTokenSource, Disposable, InlineCompletionItem } from '../util/protocol' import { byteIndex, toText } from '../util/string' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' const logger = createLogger('handler-inline') const NAMESPACE = 'inlineSuggest' export interface InlineSuggestOption { silent?: boolean provider?: string autoTrigger?: boolean } export interface InlineSuggestConfig { autoTrigger: boolean triggerCompletionWait: number } export type AcceptKind = 'all' | 'word' | 'line' export function formatInsertText(text: string, opts: FormattingOptions): string { let lines = text.split(/\r?\n/) let tabSize = defaultValue(opts.tabSize, 2) let ind = opts.insertSpaces ? ' '.repeat(opts.tabSize) : '\t' lines = lines.map(line => { let space = line.match(/^\s*/)[0] let isTab = space.startsWith('\t') let len = space.length if (isTab && opts.insertSpaces) { space = ind.repeat(space.length) } else if (!isTab && !opts.insertSpaces) { space = ind.repeat(space.length / tabSize) } return space + line.slice(len) }) return lines.join('\n') } export function getInsertText(item: InlineCompletionItem, formatOptions: FormattingOptions): string { if (StringValue.isSnippet(item.insertText)) { const parser = new SnippetParser(false) const snippet = parser.parse(item.insertText.value, true) return formatInsertText(snippet.toString(), formatOptions) } return formatInsertText(item.insertText, formatOptions) } export function getInserted(curr: string, synced: string, character: number): { start: number, text: string } | undefined { if (curr.length < synced.length) return undefined let after = curr.slice(character) if (!synced.endsWith(after)) return undefined let start = synced.length - after.length if (!curr.startsWith(synced.slice(0, start))) return undefined return { start, text: curr.slice(start, character) } } export function getPumInserted(document: Document, cursor: Position): string | undefined { const { line, character } = cursor let synced = toText(document.textDocument.lines[line]) let curr = document.getline(cursor.line) if (synced === curr) return '' let change = getInserted(curr, synced, character) return change ? change.text : undefined } export function checkInsertedAtBeginning(currentLine: string, triggerCharacter: number, inserted: string, item: InlineCompletionItem): boolean { if (!item.range) { // check if inserted string is at the beginning item's insertText if (StringValue.isSnippet(item.insertText)) { return item.insertText.value.startsWith(inserted) } return item.insertText.startsWith(inserted) } // check if inserted string is at the beginning of item's range let current = currentLine.slice(item.range.start.character, triggerCharacter + inserted.length) if (StringValue.isSnippet(item.insertText)) { return item.insertText.value.startsWith(current) } return item.insertText.startsWith(current) } function fixRange(range: Range | undefined, inserted: string | undefined): Range | undefined { if (!inserted || !range) return range return Range.create(range.start, Position.create(range.end.line, range.end.character + inserted.length)) } export class InlineSession { constructor( public readonly bufnr: number, public readonly cursor: Position, public readonly items: InlineCompletionItem[], public index = 0, public vtext: string | undefined = undefined ) { } public get length(): number { return this.items.length } public get selected(): InlineCompletionItem | undefined { return this.items[this.index] } public clearNamespace(): void { if (this.vtext) { workspace.nvim.createBuffer(this.bufnr).clearNamespace(NAMESPACE) this.vtext = undefined } } public get extra(): string { return this.length > 1 ? `(${this.index + 1}/${this.length})` : '' } public get nextIndex(): number { return this.index === this.length - 1 ? 0 : this.index + 1 } public get prevIndex(): number { return this.index === 0 ? this.length - 1 : this.index - 1 } } export default class InlineCompletion { public session: InlineSession | undefined private bufnr: number private tokenSource: CancellationTokenSource private disposables: Disposable[] = [] private config: InlineSuggestConfig private _applying = false private _inserted: string | undefined constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) window.onDidChangeActiveTextEditor(() => { this.loadConfiguration() }, this, this.disposables) const triggerOption = { autoTrigger: true } workspace.onDidChangeTextDocument(e => { if (languages.inlineCompletionItemManager.isEmpty === false && this.config.autoTrigger && e.bufnr === defaultValue(window.activeTextEditor, {} as any).bufnr && !this._applying && events.insertMode ) { const wait = this.config.triggerCompletionWait this.trigger(e.bufnr, triggerOption, wait).catch(onUnexpectedError) } }, null, this.disposables) events.on('TextChangedI', bufnr => { // Try trigger on pum navigate. if (events.pumInserted && !languages.inlineCompletionItemManager.isEmpty) { const wait = this.config.triggerCompletionWait this.trigger(bufnr, triggerOption, wait).catch(onUnexpectedError) } }, null, this.disposables) events.on('ModeChanged', ev => { if (!ev.new_mode.startsWith('i')) { this.cancel() } }, null, this.disposables) events.on('InsertCharPre', () => { this.cancel() }, null, this.disposables) events.on('LinesChanged', bufnr => { if (bufnr === this.bufnr) { this.cancel() } }, null, this.disposables) workspace.onDidCloseTextDocument(e => { if (e.bufnr === this.bufnr) { this.cancel() } }, null, this.disposables) commands.titles.set('document.checkInlineCompletion', 'check inline completion state of current buffer') this.handler.addDisposable(commands.registerCommand('document.checkInlineCompletion', async () => { if (!this.supported) { void window.showWarningMessage(`Inline completion is not supported on current vim ${workspace.env.version}`) return } let bufnr = await this.nvim.eval('bufnr("%")') as number let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) { void window.showWarningMessage(`Buffer ${bufnr} is not attached, see ':h coc-document-attached'.`) return } let disable = await nvim.createBuffer(bufnr).getVar('coc_inline_disable') as number if (disable == 1) { void window.showWarningMessage(`Trigger inline completion is disabled by b:coc_inline_disable.`) return } let providers = languages.inlineCompletionItemManager.getProviders(doc.textDocument) if (providers.length === 0) { void window.showWarningMessage(`Inline completion provider not found for buffer ${bufnr}.`) return } let names = providers.map(item => item.provider['__extensionName'] ?? 'unknown') void window.showInformationMessage(`Inline completion is supported by ${names.join(', ')}.`) })) } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('inlineSuggest')) { let doc = defaultValue(window.activeTextEditor, {}).document let config = workspace.getConfiguration('inlineSuggest', doc) let autoTrigger = defaultValue(config.inspect('autoTrigger').globalValue as boolean, true) this.config = Object.assign(this.config ?? {}, { autoTrigger, triggerCompletionWait: defaultValue(config.inspect('triggerCompletionWait').globalValue as number, 10) }) } } public get supported(): boolean { return workspace.has('patch-9.0.0185') || workspace.has('nvim-0.7.0') } public get selected(): InlineCompletionItem | undefined { return this.session?.selected } public async visible(): Promise { let result = await this.nvim.call('coc#inline#visible') as number return !!result } public get vtextBufnr(): number { return this.session?.vtext == null ? -1 : this.session.bufnr } public async trigger(bufnr: number, option?: InlineSuggestOption, delay?: number): Promise { if (!this.supported) return false this.cancel() option = option ?? {} let document = workspace.getDocument(bufnr) if (!document || !document.attached || !languages.hasProvider(ProviderName.InlineCompletion, document)) return false let tokenSource = this.tokenSource = new CancellationTokenSource() this.bufnr = bufnr this._inserted = undefined let token = tokenSource.token if (delay) await waitWithToken(delay, token) if (option.autoTrigger !== true && document.hasChanged) { this._applying = true await document.synchronize() this._applying = false } if (token.isCancellationRequested) return false let state = await this.handler.getCurrentState() let disable = await document.buffer.getVar('coc_inline_disable') as number if (disable == 1 || state.doc.bufnr !== bufnr || !state.mode.startsWith('i') || token.isCancellationRequested) return false let cursor = state.position let triggerPosition = cursor let curr = document.getline(cursor.line) if (option.autoTrigger) { let inserted = this._inserted = getPumInserted(document, cursor) if (inserted == null) return false triggerPosition = Position.create(cursor.line, cursor.character - inserted.length) } const selectedCompletionInfo = completion.selectedCompletionInfo if (selectedCompletionInfo && this._inserted) selectedCompletionInfo.range.end.character -= this._inserted.length let items = await languages.provideInlineCompletionItems(document.textDocument, triggerPosition, { provider: option.provider, selectedCompletionInfo, triggerKind: option.autoTrigger ? InlineCompletionTriggerKind.Automatic : InlineCompletionTriggerKind.Invoked }, token) this.tokenSource = undefined if (!Array.isArray(items) || token.isCancellationRequested) return false items = items.filter(item => !item.range || positionInRange(triggerPosition, item.range) === 0) // Inserted by pum navigate if (this._inserted) items = items.filter(item => checkInsertedAtBeginning(curr, triggerPosition.character, this._inserted, item)) if (items.length === 0) { if (!option.autoTrigger && !option.silent) { void window.showWarningMessage(`No inline completion items from provider.`) } return false } this.session = new InlineSession(bufnr, cursor, items) await this.insertVtext(items[0]) return true } public async accept(bufnr: number, kind: AcceptKind = 'all'): Promise { if (bufnr !== this.vtextBufnr || !this.selected) return false let item = this.selected let cursor = this.session.cursor let insertedText = this.session.vtext this.cancel() let doc = workspace.getAttachedDocument(bufnr) let insertedLength = 0 const itemRange = fixRange(item.range, this._inserted) if (StringValue.isSnippet(item.insertText) && kind == 'all') { let range = defaultValue(itemRange, Range.create(cursor, cursor)) let text = item.insertText.value if (!itemRange && this._inserted) text = text.slice(this._inserted.length) let edit = TextEdit.replace(range, text) await commands.executeCommand('editor.action.insertSnippet', edit) } else { let range = Range.create(cursor, cursor) if (kind == 'word') { let total = 0 for (let i = 1; i < insertedText.length; i++) { if (doc.isWord(insertedText[i])) { total = i } else { break } } insertedText = insertedText.slice(0, total + 1) insertedLength = insertedText.length } else if (kind == 'line') { // get the first line of insertedText const insertText = insertedText.split('\n')[0] insertedLength = insertText.length } else { insertedText = getInsertText(item, window.activeTextEditor.options) if (itemRange) { range = itemRange } else if (this._inserted) { insertedText = insertedText.slice(this._inserted.length) } } await doc.applyEdits([TextEdit.replace(range, insertedText)], false, false) await window.moveTo(getEnd(range.start, insertedText)) } if (item.command) { try { await commands.execute(item.command) } catch (err) { logger.error(`Error on execute command "${item.command.command}"`, err) } } await events.fire('InlineAccept', [insertedLength, item]) return true } public async next(bufnr: number): Promise { await this._navigate(true, bufnr) } public async prev(bufnr: number): Promise { await this._navigate(false, bufnr) } private async _navigate(next: boolean, bufnr: number): Promise { if (bufnr !== this.vtextBufnr || this.session.length <= 1) return let idx = next ? this.session.nextIndex : this.session.prevIndex this.session.index = idx await this.insertVtext(this.session.selected) } public async insertVtext(item: InlineCompletionItem): Promise { if (!this.session || !item) return const { bufnr, extra, cursor } = this.session let doc = workspace.getDocument(bufnr) let formatOptions = window.activeTextEditor.options let text = getInsertText(item, formatOptions) const line = doc.getline(cursor.line) const itemRange = fixRange(item.range, this._inserted) if (itemRange && !emptyRange(itemRange)) { let current = line.slice(itemRange.start.character, cursor.character) text = text.slice(current.length) if (comparePosition(cursor, itemRange.end) !== 0) { let after = line.slice(cursor.character, itemRange.end.character) if (text.endsWith(after)) { text = text.slice(0, -after.length) } } } else if (this._inserted) { text = text.slice(this._inserted.length) } const col = byteIndex(line, cursor.character) + 1 let shown = await this.nvim.call('coc#inline#_insert', [bufnr, cursor.line, col, text.split('\n'), extra]) if (!this.session) return if (shown) { this.session.vtext = text this.nvim.redrawVim() void events.fire('InlineShown', [item]) } else { this.session.clearNamespace() this.session = undefined } } public cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource.dispose() this.tokenSource = undefined } if (this.session) { this.session.clearNamespace() this.session = undefined } this.bufnr = undefined } public dispose(): void { disposeAll(this.disposables) } } ================================================ FILE: src/handler/linkedEditing.ts ================================================ 'use strict' import { Neovim, Window } from '@chemzqm/neovim' import { Position, TextEdit } from 'vscode-languageserver-types' import TextRange from '../cursors/textRange' import { getBeforeCount, getChange, getDelta } from '../cursors/util' import events from '../events' import languages, { ProviderName } from '../languages' import Document from '../model/document' import { DidChangeTextDocumentParams } from '../types' import { getConditionValue } from '../util' import { debounce } from '../util/node' import { emptyRange, positionInRange, rangeAdjacent, rangeInRange, rangeIntersect } from '../util/position' import { CancellationTokenSource } from '../util/protocol' import { characterIndex } from '../util/string' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' const debounceTime = getConditionValue(200, 10) export default class LinkedEditingHandler { private changing = false private window: Window | undefined private bufnr: number | undefined private ranges: TextRange[] | undefined private wordPattern: string | undefined private tokenSource: CancellationTokenSource | undefined public checkPosition: ((bufnr: number, cursor: [number, number]) => void) & { clear(): void } constructor(private nvim: Neovim, handler: HandlerDelegate) { this.checkPosition = debounce(this._checkPosition, debounceTime) handler.addDisposable(events.on('CursorMoved', (bufnr, cursor) => { this.cancel() this.checkPosition(bufnr, [cursor[0], cursor[1]]) })) handler.addDisposable(events.on('CursorMovedI', (bufnr, cursor) => { this.cancel() this.checkPosition(bufnr, [cursor[0], cursor[1]]) })) handler.addDisposable(window.onDidChangeActiveTextEditor(() => { this.cancel() this.cancelEdit() })) handler.addDisposable(events.on('InsertCharPre', (character, bufnr) => { if (bufnr !== this.bufnr) return let doc = workspace.getDocument(bufnr) if (!this.wordPattern) { if (!doc.isWord(character) && character !== '-') this.cancelEdit() } else { let r = new RegExp(this.wordPattern) if (!r.test(character)) this.cancelEdit() } })) handler.addDisposable(workspace.onDidChangeTextDocument(async e => { await this.onChange(e) })) } private cancelEdit(): void { this.window?.clearMatchGroup('^CocLinkedEditing') this.ranges = undefined this.window = undefined this.bufnr = undefined } public async onChange(e: DidChangeTextDocumentParams): Promise { if (e.bufnr !== this.bufnr || this.changing || !this.ranges) return if (e.contentChanges.length === 0) { this.doHighlights() return } let change = e.contentChanges[0] let { text, range } = change let affected = this.ranges.filter(r => { if (!rangeIntersect(range, r.range)) return false if (rangeAdjacent(range, r.range)) { if (text.includes('\n') || !emptyRange(range)) return false } return true }) if (affected.length == 1 && rangeInRange(range, affected[0].range)) { if (text.includes('\n')) { this.cancelEdit() return } // change textRange await this.applySingleEdit(affected[0], { range, newText: text }) } else { this.cancelEdit() } } private async applySingleEdit(textRange: TextRange, edit: TextEdit): Promise { // single range change, calculate & apply changes for all ranges let { bufnr, ranges } = this let doc = workspace.getDocument(bufnr) let after = ranges.filter(r => r !== textRange && r.position.line == textRange.position.line) after.forEach(r => r.adjustFromEdit(edit)) let change = getChange(textRange, edit.range, edit.newText) let delta = getDelta(change) ranges.forEach(r => r.applyChange(change)) let edits = ranges.filter(r => r !== textRange).map(o => o.textEdit) // logger.debug('edits:', JSON.stringify(edits, null, 2)) this.changing = true await doc.applyEdits(edits, true, true) this.changing = false if (delta != 0) { for (let r of ranges) { let n = getBeforeCount(r, this.ranges, textRange) r.move(n * delta) } } this.doHighlights() } private doHighlights(): void { let { window, ranges, nvim } = this if (window && ranges) { nvim.pauseNotification() window.clearMatchGroup('^CocLinkedEditing') window.highlightRanges('CocLinkedEditing', ranges.map(o => o.range), 99, true) nvim.resumeNotification(true, true) } } private _checkPosition(bufnr: number, cursor: [number, number]): void { if (events.completing || !workspace.isAttached(bufnr)) return let doc = workspace.getDocument(bufnr) let config = workspace.getConfiguration('coc.preferences', doc) let enabled = config.get('enableLinkedEditing', false) if (!enabled || !languages.hasProvider(ProviderName.LinkedEditing, doc.textDocument)) return let character = characterIndex(doc.getline(cursor[0] - 1), cursor[1] - 1) let position = Position.create(cursor[0] - 1, character) if (this.ranges) { if (this.ranges.some(r => positionInRange(position, r.range) == 0)) { return } this.cancelEdit() } void this.enable(doc, position) } public async enable(doc: Document, position: Position): Promise { let textDocument = doc.textDocument let tokenSource = this.tokenSource = new CancellationTokenSource() let token = tokenSource.token let win = await this.nvim.window let linkedRanges = await languages.provideLinkedEdits(textDocument, position, token) if (token.isCancellationRequested || !linkedRanges || linkedRanges.ranges.length == 0) return let ranges = linkedRanges.ranges.map(o => new TextRange(o.start.line, o.start.character, textDocument.getText(o))) this.wordPattern = linkedRanges.wordPattern this.bufnr = doc.bufnr this.window = win this.ranges = ranges this.doHighlights() } private cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } } ================================================ FILE: src/handler/links.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import debounce from 'debounce' import { DocumentLink, Range } from 'vscode-languageserver-types' import { IConfigurationChangeEvent } from '../configuration/types' import events from '../events' import languages, { ProviderName } from '../languages' import BufferSync, { SyncItem } from '../model/bufferSync' import Document from '../model/document' import { DidChangeTextDocumentParams, Documentation, FloatFactory, HighlightItem } from '../types' import { disposeAll, getConditionValue } from '../util' import { isFalsyOrEmpty, toArray } from '../util/array' import { equals } from '../util/object' import { positionInRange } from '../util/position' import { CancellationTokenSource, Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' // const regex = /CocAction(Async)?\(["']openLink["']\)/ let floatFactory: FloatFactory | undefined const debounceTime = getConditionValue(200, 10) const NAMESPACE = 'links' const highlightGroup = 'CocLink' interface LinkConfig { enable: boolean highlight: boolean } export default class Links implements Disposable { private disposables: Disposable[] = [] private tooltip: boolean private tokenSource: CancellationTokenSource private buffers: BufferSync constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.setConfiguration() workspace.onDidChangeConfiguration(this.setConfiguration, this, this.disposables) events.on('CursorHold', async () => { await this.showTooltip() }, null, this.disposables) events.on(['CursorMoved', 'InsertEnter'], () => { this.cancel() }, null, this.disposables) this.buffers = workspace.registerBufferSync(doc => { return new LinkBuffer(doc) }) this.disposables.push(this.buffers) languages.onDidLinksRefresh(selector => { for (let item of this.buffers.items) { if (workspace.match(selector, item.doc)) { item.fetchLinks() } } }, null, this.disposables) } private setConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('links')) { this.tooltip = workspace.initialConfiguration.get('links.tooltip', false) if (e) { for (let item of this.buffers.items) { item.updateDocumentConfig() } } } } public async showTooltip(): Promise { if (!this.tooltip) return let link = await this.getCurrentLink() if (!link || !link.target) return let text = link.target if (link.tooltip) text += ' ' + link.tooltip let doc: Documentation = { content: text, filetype: 'txt' } if (!floatFactory) floatFactory = window.createFloatFactory({}) await floatFactory.show([doc]) } public async getLinks(): Promise> { let { doc } = await this.handler.getCurrentState() let buf = this.buffers.getItem(doc.bufnr) await buf.getLinks() return toArray(buf.links) } public async getCurrentLink(): Promise { let links = await this.getLinks() let pos = await window.getCursorPosition() if (links && links.length) { for (let link of links) { if (positionInRange(pos, link.range) == 0) { if (!link.target) { let tokenSource = this.tokenSource = this.tokenSource || new CancellationTokenSource() link = await languages.resolveDocumentLink(link, this.tokenSource.token) this.tokenSource = undefined if (!link.target || tokenSource.token.isCancellationRequested) continue } return link } } } let line = await this.nvim.call('getline', ['.']) as string let regex = /\w+?:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9@:%_\\+.~#?&//=]*/gi let arr let link: DocumentLink | undefined while ((arr = regex.exec(line)) !== null) { let start = arr.index if (start <= pos.character && start + arr[0].length >= pos.character) { link = DocumentLink.create(Range.create(pos.line, start, pos.line, start + arr[0].length), arr[0]) break } } return link } public async openCurrentLink(): Promise { let link = await this.getCurrentLink() if (link) { await this.openLink(link) return true } return false } public async openLink(link: DocumentLink): Promise { if (!link.target) throw new Error(`Failed to resolve link target`) await workspace.openResource(link.target) } public getBuffer(bufnr: number): LinkBuffer | undefined { return this.buffers.getItem(bufnr) } private cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } public dispose(): void { disposeAll(this.disposables) } } class LinkBuffer implements SyncItem { private currentVersion = -1 private tokenSource: CancellationTokenSource | undefined private _config: LinkConfig | undefined public links: DocumentLink[] = [] public fetchLinks: (() => void) & { clear(): void } // last highlight version constructor(public readonly doc: Document) { this.fetchLinks = debounce(() => { void this.getLinks() }, debounceTime) if (this.hasProvider) this.fetchLinks() } public get config(): LinkConfig { if (this._config) return this._config this.updateDocumentConfig() return this._config } private get hasProvider(): boolean { return languages.hasProvider(ProviderName.DocumentLink, this.doc) } public updateDocumentConfig(): void { let configuration = workspace.getConfiguration('links', this.doc) this._config = { enable: configuration.get('enable', true), highlight: configuration.get('highlight', false), } } public onChange(e: DidChangeTextDocumentParams): void { if (e.contentChanges.length == 0) { this.highlight() } else { this.cancel() this.fetchLinks() } } public highlight(): void { if (!this.config.highlight || !this.links) return let { links, doc } = this if (isFalsyOrEmpty(links)) { this.clearHighlight() } else { let highlights: HighlightItem[] = [] links.forEach(link => { doc.addHighlights(highlights, highlightGroup, link.range) }) this.doc.buffer.updateHighlights(NAMESPACE, highlights, { priority: 2048 }) } } public clearHighlight(): void { this.buffer.clearNamespace(NAMESPACE) } public get buffer(): Buffer { return this.doc.buffer } public cancel(): void { this.fetchLinks.clear() if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } public async getLinks(): Promise { if (!this.hasProvider || !this.config.enable || this.currentVersion === this.doc.version) return this.currentVersion = this.doc.version this.cancel() let tokenSource = this.tokenSource = new CancellationTokenSource() let token = tokenSource.token let links = await languages.getDocumentLinks(this.doc.textDocument, token) this.tokenSource = undefined if (token.isCancellationRequested || sameLinks(toArray(this.links), toArray(links))) return this.links = toArray(links) this.highlight() } public dispose(): void { this.cancel() } } export function sameLinks(links: ReadonlyArray, other: ReadonlyArray): boolean { if (links.length != other.length) return false for (let i = 0; i < links.length; i++) { if (!equals(links[i].range, other[i].range)) { return false } } return true } ================================================ FILE: src/handler/locations.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { TextDocument } from 'vscode-languageserver-textdocument' import { Location, LocationLink, Position } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import languages, { ProviderName } from '../languages' import services from '../services' import { LocationWithTarget } from '../types' import { isFalsyOrEmpty } from '../util/array' import { hasOwnProperty } from '../util/object' import { CancellationToken, CancellationTokenSource } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' export interface TagDefinition { name: string cmd: string filename: string } export type RequestFunc = (doc: TextDocument, position: Position, token: CancellationToken) => Thenable export default class LocationsHandler { constructor(private nvim: Neovim, private handler: HandlerDelegate) { } private async request(method: ProviderName, fn: RequestFunc): Promise { let { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(method, doc.textDocument) await doc.synchronize() return await this.handler.withRequestToken(method, token => { return fn(doc.textDocument, position, token) }, true) } public async definitions(): Promise { const { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.Definition, doc.textDocument) await doc.synchronize() const tokenSource = new CancellationTokenSource() return languages.getDefinition(doc.textDocument, position, tokenSource.token) } public async declarations(): Promise { const { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.Declaration, doc.textDocument) await doc.synchronize() const tokenSource = new CancellationTokenSource() return languages.getDeclaration(doc.textDocument, position, tokenSource.token) } public async typeDefinitions(): Promise { const { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.TypeDefinition, doc.textDocument) await doc.synchronize() const tokenSource = new CancellationTokenSource() return languages.getTypeDefinition(doc.textDocument, position, tokenSource.token) } public async implementations(): Promise { const { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.Implementation, doc.textDocument) await doc.synchronize() const tokenSource = new CancellationTokenSource() return languages.getImplementation(doc.textDocument, position, tokenSource.token) } public async references(excludeDeclaration?: boolean): Promise { const { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.Reference, doc.textDocument) await doc.synchronize() const tokenSource = new CancellationTokenSource() return languages.getReferences(doc.textDocument, { includeDeclaration: !excludeDeclaration }, position, tokenSource.token) } public async gotoDefinition(openCommand?: string | false): Promise { let definition = await this.request(ProviderName.Definition, (doc, position, token) => { return languages.getDefinition(doc, position, token) }) await this.handleLocations(definition, openCommand) return !isFalsyOrEmpty(definition) } public async gotoDeclaration(openCommand?: string | false): Promise { let definition = await this.request(ProviderName.Declaration, (doc, position, token) => { return languages.getDeclaration(doc, position, token) }) await this.handleLocations(definition, openCommand) return !isFalsyOrEmpty(definition) } public async gotoTypeDefinition(openCommand?: string | false): Promise { let definition = await this.request(ProviderName.TypeDefinition, (doc, position, token) => { return languages.getTypeDefinition(doc, position, token) }) await this.handleLocations(definition, openCommand) return !isFalsyOrEmpty(definition) } public async gotoImplementation(openCommand?: string | false): Promise { let definition = await this.request(ProviderName.Implementation, (doc, position, token) => { return languages.getImplementation(doc, position, token) }) await this.handleLocations(definition, openCommand) return !isFalsyOrEmpty(definition) } public async gotoReferences(openCommand?: string | false, includeDeclaration = true): Promise { let definition = await this.request(ProviderName.Reference, (doc, position, token) => { return languages.getReferences(doc, { includeDeclaration }, position, token) }) await this.handleLocations(definition, openCommand) return !isFalsyOrEmpty(definition) } public async getTagList(): Promise { let bufnr = await this.nvim.call('bufnr', '%') as number let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) return null let position = await window.getCursorPosition() let word = await this.nvim.call('expand', '') as string if (!word) return null if (!languages.hasProvider(ProviderName.Definition, doc.textDocument)) return null let tokenSource = new CancellationTokenSource() let definitions = [] try { let timeout = workspace.initialConfiguration.get('coc.preferences.tagDefinitionTimeout', 0) if (timeout > 0) { const abort = new Promise<[]>((_, rej) => setTimeout(() => rej(new Error('timeout')), timeout)) definitions = await Promise.race([languages.getDefinition(doc.textDocument, position, tokenSource.token), abort]) } else { definitions = await languages.getDefinition(doc.textDocument, position, tokenSource.token) } } catch (e) { return null } if (!definitions || !definitions.length) return null return definitions.map(location => { let parsedURI = URI.parse(location.uri) const filename = parsedURI.scheme == 'file' ? parsedURI.fsPath : parsedURI.toString() return { name: word, cmd: `silent keepjumps call coc#cursor#move_to(${location.range.start.line}, ${location.range.start.character})`, filename, } }) } /** * Send custom request for locations to services. */ public async findLocations(id: string, method: string, params: any, openCommand: string | false = false): Promise { let { doc, position } = await this.handler.getCurrentState() params = params || {} Object.assign(params, { textDocument: { uri: doc.uri }, position }) let res: any = await services.sendRequest(id, method, params) let locations = this.toLocations(res) await this.handleLocations(locations, openCommand) return locations.length > 0 } public toLocations(location: Location | LocationLink | Location[] | LocationLink[] | null): LocationWithTarget[] { let res: LocationWithTarget[] = [] if (location && hasOwnProperty(location, 'location') && hasOwnProperty(location, 'children')) { let getLocation = (item: any): void => { if (!item) return if (Location.is(item.location)) { res.push(item.location) } else if (LocationLink.is(item.location)) { let loc = item.location as LocationLink res.push({ uri: loc.targetUri, range: loc.targetSelectionRange, targetRange: loc.targetRange }) } if (item.children && item.children.length) { for (let loc of item.children) { getLocation(loc) } } } getLocation(location) return res } if (Location.is(location)) { res.push(location) } else if (LocationLink.is(location)) { res.push({ uri: location.targetUri, range: location.targetSelectionRange, targetRange: location.targetRange }) } else if (Array.isArray(location)) { for (let loc of location) { if (Location.is(loc)) { res.push(loc) } else if (loc && typeof loc.targetUri === 'string') { res.push({ uri: loc.targetUri, range: loc.targetSelectionRange, targetRange: loc.targetRange }) } } } return res } public async handleLocations(locations: LocationWithTarget[] | null | undefined, openCommand?: string | false): Promise { if (!locations) return let len = locations.length if (len == 0) return if (len == 1 && openCommand !== false) { let { uri, range } = locations[0] await workspace.jumpTo(uri, range.start, openCommand) } else { await workspace.showLocations(locations) } } } ================================================ FILE: src/handler/refactor/buffer.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { createLogger } from '../../logger' import Document from '../../model/document' import Highlighter from '../../model/highlighter' import { DidChangeTextDocumentParams, Optional, TextDocumentContentChange } from '../../types' import { disposeAll } from '../../util' import { isParentFolder, readFileLines, sameFile } from '../../util/fs' import { omit } from '../../util/lodash' import { Mutex } from '../../util/mutex' import { fastDiff, path } from '../../util/node' import { equals } from '../../util/object' import { adjustRangePosition, emptyRange } from '../../util/position' import { Disposable } from '../../util/protocol' import { byteLength } from '../../util/string' import { getChangedLineCount, lineCountChange } from '../../util/textedit' import window from '../../window' import workspace from '../../workspace' import Changes, { LineInfo } from './changes' const logger = createLogger('handler-refactorBuffer') export const SEPARATOR = '\u3000' export interface LineChange { // zero indexed lnum: number delta: number } export interface FileRangeInfo { // line number of filepath lnum: number filepath: string lines: string[] } export interface FileChange extends FileRangeInfo { // start line 0 indexed start: number // end line 0 indexed, excluded end: number } export interface FileRange { // start lnum in refactor buffer, 1 indexed lnum: number // start line 0 indexed start: number lines: string[] // range relatived to new range highlights?: Range[] } export type FileRangeDef = Optional & { end?: number } export interface FileItem { filepath: string ranges: FileRange[] } export interface FileItemDef { filepath: string ranges: FileRangeDef[] } export interface RefactorConfig { openCommand: string beforeContext: number afterContext: number saveToFile: boolean showMenu: string } export interface RefactorBufferOpts { cwd: string winid: number fromWinid: number } export default class RefactorBuffer { private _disposed = false private _fileItems: FileItem[] = [] private mutex = new Mutex() private disposables: Disposable[] = [] private matchIds: Set = new Set() private changes: Changes private changing = false constructor( public readonly bufnr: number, private srcId: number, private nvim: Neovim, public readonly config: RefactorConfig, private opts: RefactorBufferOpts ) { this.changes = new Changes() this.disposables.push(workspace.registerLocalKeymap(bufnr, 'n', '', this.splitOpen.bind(this), true)) if (config.showMenu) { this.disposables.push(workspace.registerLocalKeymap(bufnr, 'n', config.showMenu, this.showMenu.bind(this), true)) } workspace.onDidChangeTextDocument(this.onDocumentChange, this, this.disposables) } public async showMenu(): Promise { let res = await window.showMenuPicker(['Tab open', 'Remove block']) if (res == -1) return let fileRange = await this.searchCurrentRange() if (!fileRange) return if (res == 0) { let before = await this.nvim.eval(`strpart(getline('.'), 0 ,col('.') - 1)`) as string let character = before.length let bufname = this.getAbsolutePath(fileRange.filepath) this.nvim.call('coc#util#jump', ['tabe', bufname, [fileRange.line, character]], true) } if (res == 1) { let range = this.getDeleteRange(fileRange) await this.document.applyEdits([TextEdit.del(range)]) } } public get fileItems(): FileItem[] { return this._fileItems } public getFileItem(uri: string): FileItem | undefined { let filepath = URI.parse(uri).fsPath return this._fileItems.find(o => sameFile(o.filepath, filepath)) } public getFileRange(lnum: number): FileRange & { filepath: string } { for (let item of this._fileItems) { for (let r of item.ranges) { if (r.lnum == lnum) { return Object.assign(omit(r, ['highlights']), { filepath: item.filepath }) } } } throw new Error(`File range not found at lnum: ${lnum}`) } public onChange(e: DidChangeTextDocumentParams): void { if (this.changing) return if (e.contentChanges.length === 0) { this.highlightLineNr() this.nvim.redrawVim() return } let { nvim } = this e = fixChangeParams(e) let change = e.contentChanges[0] let { original } = e if (change.range.end.line > 2) { nvim.call('setbufvar', [e.bufnr, '&modified', 1], true) } let { range, text } = change let lineChange = lineCountChange(TextEdit.replace(range, text)) if (lineChange == 0) return let edits: TextEdit[] = [TextEdit.replace(range, text)] let addRanges: LineInfo[] = [] // Check removed ranges if (!emptyRange(range) && !text.includes('\u3000')) { let sl = range.start.line let lnums: number[] = [] let lines = original.split(/\r?\n/) for (let i = 0; i < lines.length; i++) { let line = lines[i] if (line.length > 1 && line.includes('\u3000')) { lnums.push(sl + i + 1) } } if (lnums.length) { let infos: LineInfo[] = lnums.map(lnum => { return this.getFileRange(lnum) }) for (let item of this._fileItems) { item.ranges = item.ranges.filter(o => !lnums.includes(o.lnum)) } this.changes.add(infos) } } else if (emptyRange(range) && text.includes('\u3000')) { // check undo let lines = text.split(/\r?\n/) let lnums: number[] = [] let sl = range.start.line for (let i = 0; i < lines.length; i++) { let line = lines[i] if (line.length > 1 && line.includes('\u3000')) { lnums.push(sl + i + 1) } } if (lnums.length) { let res = this.changes.checkInsert(lnums) if (res) addRanges = res } } else if (text.includes('\u3000')) { // check multiple ranges change edits = this.diffChanges(original, text) edits.forEach(e => { e.range = adjustRangePosition(e.range, range.start) }) } this.adjustLnums(edits) nvim.pauseNotification() this.highlightLineNr() nvim.resumeNotification(true, true) if (addRanges.length) { addRanges.forEach(info => { let item = this._fileItems.find(o => o.filepath == info.filepath) item.ranges.push(info) }) } } private diffChanges(original: string, text: string): TextEdit[] { let edits: TextEdit[] = [] let diffs = fastDiff(original, text) let offset = 0 let orig = TextDocument.create('file:///1', '', 0, original) for (let i = 0; i < diffs.length; i++) { let diff = diffs[i] let pos = orig.positionAt(offset) if (diff[0] == fastDiff.EQUAL) { offset = offset + diff[1].length } else if (diff[0] == fastDiff.DELETE) { let end = orig.positionAt(offset + diff[1].length) if (diffs[i + 1] && diffs[i + 1][0] == fastDiff.INSERT) { let text = diffs[i + 1][1] edits.push(TextEdit.replace(Range.create(pos, end), text)) i = i + 1 } else { edits.push(TextEdit.replace(Range.create(pos, end), '')) } offset = offset + diff[1].length } else if (diff[0] == fastDiff.INSERT) { edits.push(TextEdit.insert(pos, diff[1])) } } return edits } /** * Handle changes of other buffers. */ private async onDocumentChange(e: DidChangeTextDocumentParams): Promise { if (this.changing || e.contentChanges.length === 0) return let { uri } = e.textDocument let fileItem = this.getFileItem(uri) // not affected if (!fileItem) return let { range, text } = e.contentChanges[0] let lineChange = lineCountChange(TextEdit.replace(range, text)) let edits: TextEdit[] = [] let deleteIndexes: number[] = [] // 4 cases: ignore, change lineNr, reload, remove for (let i = 0; i < fileItem.ranges.length; i++) { let r = fileItem.ranges[i] // change after range if (range.start.line >= r.start + r.lines.length) continue // change before range if (range.end.line < r.start) { r.start = r.start + lineChange continue } let textDocument = workspace.getDocument(uri).textDocument let end = r.start + r.lines.length + lineChange let newLines = textDocument.lines.slice(r.start, end) if (!newLines.length) { deleteIndexes.push(i) let replaceRange = this.getDeleteRange(r) edits.push(TextEdit.replace(replaceRange, '')) } else { r.lines = newLines let replaceRange = this.getReplaceRange(r) edits.push(TextEdit.replace(replaceRange, newLines.join('\n'))) } } if (deleteIndexes.length) { fileItem.ranges = fileItem.ranges.filter((_, i) => !deleteIndexes.includes(i)) } // clean fileItem with empty ranges this._fileItems = this._fileItems.filter(o => o.ranges && o.ranges.length > 0) if (edits.length) { this.adjustLnums(edits) this.changing = true await this.document.applyEdits(edits) this.changing = false } this.nvim.pauseNotification() this.highlightLineNr() this.buffer.setOption('modified', false, true) await this.nvim.resumeNotification(true) } private adjustLnums(edits: TextEdit[]): void { for (let item of this._fileItems) { for (let fileRange of item.ranges) { let line = fileRange.lnum - 1 fileRange.lnum += getChangedLineCount(Position.create(line, 0), edits) } } } /** * Current changed file ranges */ public async getFileChanges(): Promise { let changes: FileRangeInfo[] = [] let lines = await this.buffer.lines lines.push(SEPARATOR) // current lines let arr: string[] = [] let fsPath: string let lnum: number for (let i = 0; i < lines.length; i++) { let line = lines[i] if (line.startsWith(SEPARATOR)) { if (fsPath) { changes.push({ filepath: fsPath, lines: arr.slice(), lnum }) fsPath = undefined arr = [] } if (line.length > 1) { let ms = line.match(/^\u3000(.*)/) if (ms) { fsPath = this.getAbsolutePath(ms[1].replace(/\s+$/, '')) lnum = i + 1 arr = [] } } } else { arr.push(line) } } return changes } /** * Open line under cursor in split window */ public async splitOpen(): Promise { let { nvim } = this let win = nvim.createWindow(this.opts.fromWinid) let valid = await win.valid let before = await nvim.eval(`strpart(getline('.'), 0 ,col('.') - 1)`) as string let character = before.length let fileRange = await this.searchCurrentRange() if (fileRange) { let bufname = this.getAbsolutePath(fileRange.filepath) nvim.pauseNotification() if (valid) { nvim.call('win_gotoid', [this.opts.fromWinid], true) this.nvim.call('coc#util#jump', ['edit', bufname, [fileRange.line, character]], true) } else { this.nvim.call('coc#util#jump', ['belowright vs', bufname, [fileRange.line, character]], true) } nvim.command('normal! zz', true) await nvim.resumeNotification(true) if (!valid) { this.opts.fromWinid = await nvim.call('win_getid') as number } } } public async searchCurrentRange(): Promise { let { nvim } = this let lines = await nvim.eval('getline(1,line("."))') as string[] let len = lines.length for (let i = 0; i < len; i++) { let line = lines[len - i - 1] let ms = line.match(/^\u3000(.+)/) if (ms) { let r = this.getFileRange(len - i) return Object.assign({ line: r.start + (i == 0 ? 1 : i) - 1 }, r) } } return undefined } /** * Add FileItem to refactor buffer. */ public async addFileItems(items: FileItemDef[]): Promise { if (this._disposed) return let { cwd } = this.opts let { document } = this const release = await this.mutex.acquire() try { await document.synchronize() let count = document.lineCount let highlighter = new Highlighter() let hlRanges: Range[] = [] for (let item of items) { let ranges: FileRange[] = [] for (let range of item.ranges) { highlighter.addLine(SEPARATOR) highlighter.addLine(SEPARATOR) let lnum = count + highlighter.length highlighter.addText(`${isParentFolder(cwd, item.filepath) ? path.relative(cwd, item.filepath) : item.filepath}`) // white spaces for conceal texts let n = String(range.start + 1).length + String(range.end).length + 4 if (!this.srcId) highlighter.addText(' '.repeat(n)) let base = 0 - highlighter.length - count if (range.highlights) { hlRanges.push(...range.highlights.map(r => adjustRange(r, base))) } let { lines, start, end, highlights } = range if (!lines) { lines = await this.getLines(item.filepath, start, end) } ranges.push({ lines, lnum, start, highlights }) highlighter.addLines(lines) } if (ranges.length) { let newItem: FileItem = { filepath: item.filepath, ranges } let fileItem = this._fileItems.find(o => o.filepath == item.filepath) if (fileItem) { fileItem.ranges.push(...newItem.ranges) } else { this._fileItems.push(newItem) } } } let { nvim, buffer } = this this.changing = true nvim.pauseNotification() highlighter.render(buffer, count) this.highlightLineNr() buffer.setOption('modified', false, true) buffer.setOption('undolevels', 1000, true) if (count == 2 && hlRanges.length) { let pos = hlRanges[0].start nvim.call('coc#cursor#move_to', [pos.line, pos.character], true) } await nvim.resumeNotification(true) await document.patchChange() this.changing = false await window.cursors.addRanges(hlRanges) } catch (e) { this.changing = false logger.error(`Error on add file item:`, e) } release() } public findRange(filepath: string, lnum: number): FileRange { let item = this.fileItems.find(o => sameFile(this.getAbsolutePath(o.filepath), filepath)) let range = item.ranges.find(o => o.lnum == lnum) if (!range) throw new Error(`File range not found at lnum: ${lnum}`) return range } /** * Save changes to buffers/files, return false when no change made. */ public async save(): Promise { let { nvim } = this let doc = this.document let { buffer } = doc await doc.patchChange() let changes = await this.getFileChanges() if (!changes) return changes.sort((a, b) => a.lnum - b.lnum) // filter changes that not change let fileChanges: FileChange[] = [] for (let i = 0; i < changes.length; i++) { let change = changes[i] let range = this.findRange(change.filepath, change.lnum) if (equals(range.lines, change.lines)) continue fileChanges.push(Object.assign({ start: range.start, end: range.start + range.lines.length }, change)) range.lines = change.lines } if (fileChanges.length == 0) { await window.showInformationMessage('No change.') await buffer.setOption('modified', false) return false } let changeMap: { [uri: string]: TextEdit[] } = {} for (let change of fileChanges) { let uri = URI.file(change.filepath).toString() let edits = changeMap[uri] || [] edits.push({ range: Range.create(change.start, 0, change.end, 0), newText: change.lines.join('\n') + '\n' }) changeMap[uri] = edits } this.changing = true await workspace.applyEdit({ changes: changeMap }) this.changing = false for (let item of this.fileItems) { let uri = URI.file(this.getAbsolutePath(item.filepath)).toString() let edits = changeMap[uri] if (edits && edits.length > 0) { item.ranges.forEach(r => { r.start += getChangedLineCount(Position.create(r.start, 0), edits) }) } } nvim.pauseNotification() buffer.setOption('modified', false, true) if (this.config.saveToFile) { nvim.command('silent noa wa', true) } this.highlightLineNr() await nvim.resumeNotification() return true } private async getLines(fsPath: string, start: number, end: number): Promise { let uri = URI.file(fsPath).toString() let doc = workspace.getDocument(uri) if (doc) return doc.getLines(start, end) return await readFileLines(fsPath, start, end - 1) } private getAbsolutePath(filepath: string): string { if (path.isAbsolute(filepath)) return filepath return path.join(this.opts.cwd, filepath) } /** * Use conceal/virtual text to add lineNr */ private highlightLineNr(): void { let { fileItems, nvim, srcId, bufnr } = this let { winid, cwd } = this.opts let info = {} if (srcId) { nvim.call('nvim_buf_clear_namespace', [bufnr, srcId, 0, -1], true) for (let item of fileItems) { for (let range of item.ranges) { let end = range.start + range.lines.length let text = `${range.start + 1}:${end}` info[range.lnum] = [range.start + 1, end] nvim.call('nvim_buf_set_virtual_text', [bufnr, srcId, range.lnum - 1, [[text, 'LineNr']], {}], true) } } } else { if (this.matchIds.size) { nvim.call('coc#highlight#clear_matches', [winid, Array.from(this.matchIds)], true) this.matchIds.clear() } let id = 2000 for (let item of fileItems) { let filename = `${cwd ? path.relative(cwd, item.filepath) : item.filepath}` let col = byteLength(filename) + 1 for (let range of item.ranges) { let end = range.start + range.lines.length let text = `:${range.start + 1}:${end}` for (let i = 0; i < text.length; i++) { let ch = text[i] this.matchIds.add(id) info[range.lnum] = [range.start + 1, end] nvim.call('matchaddpos', ['Conceal', [[range.lnum, col + i]], 99, id, { conceal: ch, window: winid }], true) id++ } } } } this.buffer.setVar('line_infos', info, true) } public getDeleteRange(r: FileRange): Range { let { document } = this let start = r.lnum - 1 let end: Position let total = document.lineCount for (let i = start; i < total; i++) { if (i + 1 == total) { end = Position.create(total, 0) break } let line = document.getline(i) if (line === SEPARATOR) { end = Position.create(i + 1, 0) break } if (i != start && line.startsWith(SEPARATOR)) { end = Position.create(i, 0) break } } return Range.create(Position.create(start, 0), end) } public getReplaceRange(r: FileRange): Range { let { document } = this let start = r.lnum let end: Position let total = document.lineCount for (let i = start; i < total; i++) { let line = document.getline(i) if (i + 1 == total) { end = Position.create(i, line.length) break } let next = document.getline(i + 1) if (next.startsWith('\u3000')) { end = Position.create(i, line.length) break } } return Range.create(Position.create(start, 0), end) } public get valid(): Promise { return this.buffer.valid } public get buffer(): Buffer { return this.nvim.createBuffer(this.bufnr) } public get document(): Document | null { return workspace.getDocument(this.bufnr) } public dispose(): void { this._disposed = true disposeAll(this.disposables) } } function adjustRange(range: Range, offset: number): Range { let { start, end } = range return Range.create(start.line - offset, start.character, end.line - offset, end.character) } export function fixChangeParams(e: DidChangeTextDocumentParams): DidChangeTextDocumentParams { let { contentChanges, bufnr, textDocument, original, originalLines, document } = e let { range, text } = contentChanges[0] let changes: TextDocumentContentChange[] = [{ range, text }] if (!original) { if (emptyRange(range) && range.start.character != 0) { let lines = text.split(/\r?\n/) let last = lines[lines.length - 1] let before = originalLines[range.start.line].slice(0, range.start.character) if (last.startsWith(SEPARATOR) && before == last) { changes[0].text = before + lines.slice(0, -1).join('\n') + '\n' let { start, end } = range changes[0].range = Range.create(start.line, 0, end.line, 0) } } } else { let lines = original.split(/\r?\n/) let last = lines[lines.length - 1] if (last.startsWith(SEPARATOR)) { let before = originalLines[range.start.line].slice(0, range.start.character) if (before == last) { original = before + lines.slice(0, -1).join('\n') + '\n' let { start, end } = range changes[0].range = Range.create(start.line, 0, end.line, 0) } } let prev = originalLines[range.start.line - 1] let nest = lines.length > 1 ? lines[lines.length - 2] : '' if (last == '' && nest.startsWith(SEPARATOR) && prev == nest && range.start.character == 0 && range.end.character == 0) { original = prev + '\n' + lines.slice(0, -2).join('\n') + '\n' let { start, end } = range changes[0].range = Range.create(start.line - 1, 0, end.line - 1, 0) } } return { contentChanges: changes, bufnr, textDocument, document, original, originalLines } } ================================================ FILE: src/handler/refactor/changes.ts ================================================ 'use strict' import { equals } from '../../util/object' export interface LineInfo { filepath: string // start lnum in refactor buffer, 1 indexed lnum: number start: number lines: string[] } export type DeleteChange = Map export default class Changes { private stack: DeleteChange[] = [] public add(infos: LineInfo[]): void { let map: Map = new Map() for (let info of infos) { map.set(info.lnum, info) } this.stack.push(map) } public checkInsert(lnums: number[]): LineInfo[] | undefined { if (!this.stack.length) return undefined let last = this.stack[this.stack.length - 1] let arr = Array.from(last.keys()).sort((a, b) => a - b) if (!equals(arr, lnums)) return undefined this.stack.pop() return Array.from(last.values()) } } ================================================ FILE: src/handler/refactor/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Location, Range, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { IConfigurationChangeEvent } from '../../configuration/types' import events from '../../events' import languages, { ProviderName } from '../../languages' import { disposeAll } from '../../util' import { getFileLineCount } from '../../util/fs' import { compareRangesUsingStarts } from '../../util/position' import { Disposable, Emitter, Event } from '../../util/protocol' import { emptyWorkspaceEdit } from '../../util/textedit' import workspace from '../../workspace' import { HandlerDelegate } from '../types' import RefactorBuffer, { FileItemDef, FileRangeDef, RefactorConfig, SEPARATOR } from './buffer' import Search from './search' const name = '__coc_refactor__' let refactorId = 0 let srcId: number export default class Refactor { private buffers: Map = new Map() public config: RefactorConfig private disposables: Disposable[] = [] private readonly _onCreate = new Emitter() public readonly onCreate: Event = this._onCreate.event constructor( private nvim: Neovim, private handler: HandlerDelegate ) { this.setConfiguration() workspace.onDidChangeConfiguration(this.setConfiguration, this, this.disposables) events.on('BufUnload', bufnr => { let buf = this.buffers.get(bufnr) if (buf) { buf.dispose() this.buffers.delete(bufnr) } }, null, this.disposables) workspace.onDidChangeTextDocument(e => { let buf = this.buffers.get(e.bufnr) if (buf) buf.onChange(e) }, null, this.disposables) } public has(bufnr: number): boolean { return this.buffers.has(bufnr) } private setConfiguration(e?: IConfigurationChangeEvent): void { if (e && !e.affectsConfiguration('refactor')) return let config = workspace.getConfiguration('refactor', null) this.config = Object.assign(this.config || {}, { afterContext: config.get('afterContext', 3), beforeContext: config.get('beforeContext', 3), openCommand: config.get('openCommand', 'vsplit'), saveToFile: config.get('saveToFile', true), showMenu: config.get('showMenu', '') }) } /** * Refactor of current symbol */ public async doRefactor(): Promise { let { doc, position } = await this.handler.getCurrentState() if (!languages.hasProvider(ProviderName.Rename, doc.textDocument)) { throw new Error(`Rename provider not found for current buffer`) } await doc.synchronize() let edit = await this.handler.withRequestToken('refactor', async token => { let res = await languages.prepareRename(doc.textDocument, position, token) if (token.isCancellationRequested) return null if (res === false) throw new Error(`Provider returns null on prepare, unable to rename at current position`) let edit = await languages.provideRenameEdits(doc.textDocument, position, 'NewName', token) if (token.isCancellationRequested) return null if (!edit) throw new Error('Provider returns null for rename edits.') return edit }) if (edit) { await this.fromWorkspaceEdit(edit, doc.filetype) } } /** * Search by rg */ public async search(args: string[]): Promise { let buf = await this.createRefactorBuffer() let cwd = await this.nvim.call('getcwd', []) as string let search = new Search(this.nvim) await search.run(args, cwd, buf) } public async save(bufnr: number): Promise { let buf = this.buffers.get(bufnr) if (buf) return await buf.save() } public getBuffer(bufnr: number): RefactorBuffer { return this.buffers.get(bufnr) } /** * Create initialized refactor buffer */ public async createRefactorBuffer(filetype?: string, conceal = false): Promise { let { nvim } = this let [fromWinid, cwd] = await nvim.eval('[win_getid(),getcwd()]') as [number, string] let { openCommand } = this.config if (!nvim.isVim && !srcId) srcId = await this.nvim.createNamespace('coc-refactor') nvim.pauseNotification() nvim.command(`${openCommand} ${name}${refactorId++}`, true) nvim.command(`setl buftype=acwrite nobuflisted bufhidden=wipe nofen wrap conceallevel=2 concealcursor=n`, true) nvim.command(`setl undolevels=-1 nolist nospell noswapfile foldmethod=expr foldexpr=coc#util#refactor_foldlevel(v:lnum)`, true) nvim.command(`setl foldtext=coc#util#refactor_fold_text(v:foldstart)`, true) nvim.call('setline', [1, ['Save current buffer to make changes', SEPARATOR]], true) nvim.call('matchadd', ['Comment', '\\%1l'], true) nvim.call('matchadd', ['Conceal', '^\\%u3000'], true) nvim.call('matchadd', ['Label', '^\\%u3000\\zs\\S\\+'], true) nvim.command('setl nomod', true) if (filetype) nvim.command(`runtime! syntax/${filetype}.vim`, true) nvim.call('coc#util#do_autocmd', ['CocRefactorOpen'], true) await nvim.resumeNotification() let [bufnr, win] = await nvim.eval('[bufnr("%"),win_getid()]') as [number, number] let opts = { fromWinid, winid: win, cwd } await workspace.document let buf = new RefactorBuffer(bufnr, conceal ? undefined : srcId, this.nvim, this.config, opts) this.buffers.set(bufnr, buf) return buf } /** * Create refactor buffer from lines */ public async fromLines(lines: string[]): Promise { let buf = await this.createRefactorBuffer() await buf.buffer.setLines(lines, { start: 0, end: -1, strictIndexing: false }) return buf } /** * Create refactor buffer from locations */ public async fromLocations(locations: Location[], filetype?: string): Promise { if (!locations || locations.length == 0) return undefined let changes: { [uri: string]: TextEdit[] } = {} let edit: WorkspaceEdit = { changes } for (let location of locations) { let edits: TextEdit[] = changes[location.uri] || [] edits.push({ range: location.range, newText: '' }) changes[location.uri] = edits } return await this.fromWorkspaceEdit(edit, filetype) } /** * Start refactor from workspaceEdit */ public async fromWorkspaceEdit(edit: WorkspaceEdit, filetype?: string): Promise { if (!edit || emptyWorkspaceEdit(edit)) return undefined let items: FileItemDef[] = [] let { beforeContext, afterContext } = this.config let { changes, documentChanges } = edit const rangesMap: Map = new Map() if (documentChanges) { for (let change of documentChanges || []) { if (TextDocumentEdit.is(change)) { let { textDocument, edits } = change rangesMap.set(textDocument.uri, edits.map(o => o.range)) } } } else if (changes) { for (let [uri, edits] of Object.entries(changes)) { rangesMap.set(uri, edits.map(o => o.range)) } } for (let [key, editRanges] of rangesMap.entries()) { let max = await this.getLineCount(key) let ranges: FileRangeDef[] = [] // start end highlights let start = null let end = null let highlights: Range[] = [] editRanges.sort(compareRangesUsingStarts) for (let range of editRanges) { let { line } = range.start let s = Math.max(0, line - beforeContext) if (start != null && s < end) { end = Math.min(max, line + afterContext + 1) highlights.push(adjustRange(range, start)) } else { if (start != null) ranges.push({ start, end, highlights }) start = s end = Math.min(max, line + afterContext + 1) highlights = [adjustRange(range, start)] } } if (start != null) ranges.push({ start, end, highlights }) items.push({ ranges, filepath: URI.parse(key).fsPath }) } let buf = await this.createRefactorBuffer(filetype) await buf.addFileItems(items) return buf } private async getLineCount(uri: string): Promise { let doc = workspace.getDocument(uri) if (doc) return doc.lineCount return await getFileLineCount(URI.parse(uri).fsPath) } public reset(): void { for (let buf of this.buffers.values()) { buf.dispose() } this.buffers.clear() } public dispose(): void { this._onCreate.dispose() this.buffers.clear() disposeAll(this.disposables) } } function adjustRange(range: Range, offset: number): Range { let { start, end } = range return Range.create(start.line - offset, start.character, end.line - offset, end.character) } ================================================ FILE: src/handler/refactor/search.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import type { ChildProcess } from 'child_process' import { EventEmitter } from 'events' import { Range } from 'vscode-languageserver-types' import { createLogger } from '../../logger' import Highlighter from '../../model/highlighter' import { ansiparse } from '../../util/ansiparse' import { Mutex } from '../../util/mutex' import { child_process, path, readline } from '../../util/node' import window from '../../window' import RefactorBuffer, { FileItem, FileItemDef } from './buffer' const { spawn } = child_process const logger = createLogger('handler-search') const defaultArgs = ['--color', 'ansi', '--colors', 'path:fg:black', '--colors', 'line:fg:green', '--colors', 'match:fg:red', '--no-messages', '--heading', '-n'] const controlCode = '\x1b' // emit FileItem class Task extends EventEmitter { private process: ChildProcess public start(cmd: string, args: string[], cwd: string): void { this.process = spawn(cmd, args, { cwd, shell: process.platform === 'win32' }) this.process.on('error', e => { this.emit('error', e.message) }) const rl = readline.createInterface(this.process.stdout) let start: number let fileItem: FileItemDef let lines: string[] = [] let highlights: Range[] = [] let create = true rl.on('line', content => { if (content.includes(controlCode)) { let items = ansiparse(content) if (items.length == 0) return if (items[0].foreground == 'black') { fileItem = { filepath: path.join(cwd, items[0].text), ranges: [] } return } let normalLine = items[0].foreground == 'green' if (normalLine) { let lnum = parseInt(items[0].text, 10) - 1 let padlen = items[0].text.length + 1 if (create) { start = lnum create = false } let line = '' for (let item of items) { if (item.foreground == 'red') { let l = lnum - start let c = line.length - padlen highlights.push(Range.create(l, c, l, c + item.text.length)) } line += item.text } let currline = line.slice(padlen) lines.push(currline) } } else { let fileEnd = content.trim().length == 0 if (fileItem && (fileEnd || content.trim() == '--')) { fileItem.ranges.push({ lines, highlights, start }) } if (fileEnd) { this.emit('item', fileItem) fileItem = null } lines = [] highlights = [] create = true } }) rl.on('close', () => { if (fileItem) { if (lines.length) { fileItem.ranges.push({ lines, highlights, start, }) } this.emit('item', fileItem) } lines = highlights = fileItem = null this.emit('end') }) } public dispose(): void { if (this.process) { this.process.kill() } } } export default class Search { private task: Task constructor(private nvim: Neovim, private cmd = 'rg') { } public run(args: string[], cwd: string, refactorBuf: RefactorBuffer): Promise { let { nvim, cmd } = this let { afterContext, beforeContext } = refactorBuf.config let argList = ['-A', afterContext.toString(), '-B', beforeContext.toString()].concat(defaultArgs, args) let p = getPathFromArgs(args) if (p) argList.pop() argList.push('--', p ? path.isAbsolute(p) ? p : `./${p.replace(/^\.\//, '')}` : './') this.task = new Task() this.task.start(cmd, argList, cwd) let mutex: Mutex = new Mutex() let files = 0 let matches = 0 let start = Date.now() // remaining items let fileItems: FileItem[] = [] const addFileItems = async () => { if (fileItems.length == 0) return let items = fileItems.slice() fileItems = [] const release = await mutex.acquire() try { await refactorBuf.addFileItems(items) } catch (e) { logger.error(e) } release() } return new Promise((resolve, reject) => { let interval = setInterval(addFileItems, 300) this.task.on('item', async (fileItem: FileItem) => { files++ matches = matches + fileItem.ranges.reduce((p, r) => p + r.highlights.length, 0) fileItems.push(fileItem) }) this.task.on('error', message => { clearInterval(interval) void window.showErrorMessage(`Error on command "${cmd}": ${message}`) this.task = null reject(new Error(message)) }) this.task.on('end', async () => { clearInterval(interval) try { await addFileItems() const release = await mutex.acquire() release() this.task.removeAllListeners() this.task = null let buf = refactorBuf.buffer if (buf) { nvim.pauseNotification() if (files == 0) { buf.setLines(['No match found'], { start: 1, end: 2, strictIndexing: false }, true) // eslint-disable-next-line @typescript-eslint/no-floating-promises buf.addHighlight({ line: 1, srcId: -1, colEnd: -1, colStart: 0, hlGroup: 'Error' }) buf.setOption('modified', false, true) } else { let highlighter = new Highlighter() highlighter.addText('Files', 'MoreMsg') highlighter.addText(': ') highlighter.addText(`${files} `, 'Number') highlighter.addText('Matches', 'MoreMsg') highlighter.addText(': ') highlighter.addText(`${matches} `, 'Number') highlighter.addText('Duration', 'MoreMsg') highlighter.addText(': ') highlighter.addText(`${Date.now() - start}ms`, 'Number') highlighter.render(buf, 1, 2) } buf.setOption('modified', false, true) nvim.resumeNotification(false, true) } } catch (e) { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject(e) return } resolve() }) }) } public abort(): void { this.task?.dispose() } } // rg requires `-- [path]` at the end export function getPathFromArgs(args: string[]): string | undefined { if (args.length < 2) return undefined let len = args.length if (args[len - 1].startsWith('-')) return undefined if (args[len - 2].startsWith('-')) return undefined return args[len - 1] } ================================================ FILE: src/handler/rename.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Range, WorkspaceEdit } from 'vscode-languageserver-types' import languages, { ProviderName } from '../languages' import { emptyRange } from '../util/position' import { CancellationTokenSource } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' export default class Rename { constructor( private nvim: Neovim, private handler: HandlerDelegate) { } public async getWordEdit(): Promise { let { doc, position } = await this.handler.getCurrentState() let range = doc.getWordRangeAtPosition(position) if (!range || emptyRange(range)) return null let curname = doc.textDocument.getText(range) if (languages.hasProvider(ProviderName.Rename, doc.textDocument)) { await doc.synchronize() let requestTokenSource = new CancellationTokenSource() let res = await languages.prepareRename(doc.textDocument, position, requestTokenSource.token) if (res !== false) { let newName = curname.startsWith('a') ? 'b' : 'a' let edit = await languages.provideRenameEdits(doc.textDocument, position, newName, requestTokenSource.token) if (edit) return edit } } void window.showInformationMessage('Rename provider not found, extract word ranges from current buffer') let ranges = doc.getSymbolRanges(curname) return { changes: { [doc.uri]: ranges.map(r => ({ range: r, newText: curname })) } } } public async rename(newName?: string): Promise { let { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.Rename, doc.textDocument) await doc.synchronize() let token = (new CancellationTokenSource()).token let res = await languages.prepareRename(doc.textDocument, position, token) if (res === false) { void window.showWarningMessage('Invalid position for rename') return false } let curname: string if (!newName) { if (Range.is(res)) { curname = doc.textDocument.getText(res) await window.moveTo(res.start) } else if (res && typeof res.placeholder === 'string') { curname = res.placeholder } else { curname = await this.nvim.eval('expand("")') as string } const config = workspace.getConfiguration('coc.preferences', null) newName = await window.requestInput('New name', config.get('renameFillCurrent', true) ? curname : '') } if (newName === '') void window.showWarningMessage('Empty word, rename canceled') if (!newName) return false let edit = await languages.provideRenameEdits(doc.textDocument, position, newName, token) if (token.isCancellationRequested || !edit) return false await workspace.applyEdit(edit) return true } } ================================================ FILE: src/handler/selectionRange.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import { Position, Range, SelectionRange } from 'vscode-languageserver-types' import languages, { ProviderName } from '../languages' import { isFalsyOrEmpty } from '../util/array' import { equals } from '../util/object' import { positionInRange } from '../util/position' import window from '../window' import { HandlerDelegate } from './types' export default class SelectionRangeHandler { private selectionRange: SelectionRange = null constructor(private nvim: Neovim, private handler: HandlerDelegate) { } public async getSelectionRanges(): Promise { let { doc, position } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.SelectionRange, doc.textDocument) await doc.synchronize() return await this.handler.withRequestToken('selection ranges', token => { return languages.getSelectionRanges(doc.textDocument, [position], token) }) } public async selectRange(visualmode: string, forward: boolean): Promise { let { doc } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.SelectionRange, doc.textDocument) let positions: Position[] = [] if (!forward && (!this.selectionRange || !visualmode)) return if (visualmode) { let range = await window.getSelectedRange(visualmode) positions.push(range.start, range.end) } else { let position = await window.getCursorPosition() positions.push(position) } if (!forward) { let curr = Range.create(positions[0], positions[1]) let { selectionRange } = this while (selectionRange && selectionRange.parent) { if (equals(selectionRange.parent.range, curr)) { break } selectionRange = selectionRange.parent } if (selectionRange && selectionRange.parent) { await window.selectRange(selectionRange.range) } return } await doc.synchronize() let selectionRanges: SelectionRange[] = await this.handler.withRequestToken('selection ranges', token => { return languages.getSelectionRanges(doc.textDocument, positions, token) }) if (isFalsyOrEmpty(selectionRanges)) return false let selectionRange: SelectionRange if (selectionRanges.length == 1) { selectionRange = selectionRanges[0] } else { let end = positions[1] ?? positions[0] let r = Range.create(positions[0], end) selectionRange = selectionRanges[0] while (selectionRange) { if (equals(r, selectionRange.range)) { selectionRange = selectionRange.parent continue } if ( positionInRange(positions[0], selectionRange.range) == 0 && positionInRange(end, selectionRange.range) == 0) { break } selectionRange = selectionRange.parent } } if (!selectionRange) return false this.selectionRange = selectionRanges[0] await window.selectRange(selectionRange.range) return true } } ================================================ FILE: src/handler/semanticTokens/buffer.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import { Range, SemanticTokens, SemanticTokensDelta, SemanticTokensLegend, uinteger } from 'vscode-languageserver-types' import languages, { ProviderName } from '../../languages' import { createLogger } from '../../logger' import { SyncItem } from '../../model/bufferSync' import Document from '../../model/document' import Regions from '../../model/regions' import { HighlightItem } from '../../types' import { getConditionValue, waitWithToken } from '../../util' import { toArray } from '../../util/array' import { CancellationError, onUnexpectedError } from '../../util/errors' import { waitImmediate } from '../../util/index' import { toNumber } from '../../util/numbers' import { CancellationToken, CancellationTokenSource, Emitter, Event } from '../../util/protocol' import { bytes, isHighlightGroupCharCode, toText } from '../../util/string' import window from '../../window' import workspace from '../../workspace' const logger = createLogger('semanticTokens-buffer') const yieldEveryMilliseconds = getConditionValue(15, 5) export const HLGROUP_PREFIX = 'CocSem' export const NAMESPACE = 'semanticTokens' export type TokenRange = [number, number, number] // line, startCol, endCol export interface SemanticTokensConfig { enable: boolean highlightPriority: number incrementTypes: string[] combinedModifiers: string[] } export interface SemanticTokenRange { range: TokenRange tokenType: string tokenModifiers: string[] hlGroup?: string combine: boolean } interface SemanticTokensPreviousResult { readonly version: number readonly resultId: string | undefined, readonly tokens?: uinteger[], } interface RangeHighlights { highlights: SemanticTokenRange[] /** * 0 based */ start: number /** * 0 based exclusive */ end: number } // should be higher than document debounce const debounceInterval = getConditionValue(100, 20) const requestDelay = getConditionValue(500, 20) const highlightGroupMap: Map = new Map() export interface StaticConfig { filetypes: string[] | null } export default class SemanticTokensBuffer implements SyncItem { private _config: SemanticTokensConfig private _dirty = false private _version: number | undefined public readonly regions = new Regions() private tokenSource: CancellationTokenSource private rangeTokenSource: CancellationTokenSource private previousResults: SemanticTokensPreviousResult | undefined private _highlights: [number, SemanticTokenRange[]] private readonly _onDidRefresh = new Emitter() public readonly onDidRefresh: Event = this._onDidRefresh.event constructor(private nvim: Neovim, public readonly doc: Document, private staticConfig: StaticConfig) { if (this.hasProvider) this.doHighlight().catch(onUnexpectedError) } public get config(): SemanticTokensConfig { if (this._config) return this._config this.loadConfiguration() return this._config } public loadConfiguration(): void { let config = workspace.getConfiguration('semanticTokens', this.doc) let changed = this._config != null && this._config.enable != config.enable this._config = { enable: config.get('enable'), highlightPriority: config.get('highlightPriority'), incrementTypes: config.get('incrementTypes'), combinedModifiers: config.get('combinedModifiers') } if (changed) { if (this._config.enable) { this.forceHighlight().catch(onUnexpectedError) } else { this.clearHighlight() } } } public get configEnabled(): boolean { let { enable } = this.config let { filetypes } = this.staticConfig // should be null when not specified if (Array.isArray(filetypes)) return filetypes.includes('*') || filetypes.includes(this.doc.filetype) return enable } public get bufnr(): number { return this.doc.bufnr } public onChange(): void { // need debounce for document synchronize this.doHighlight().catch(onUnexpectedError) } public onTextChange(): void { this._version = undefined this.cancel() } public async forceHighlight(): Promise { this.clearHighlight() await this.doHighlight(true, 0) } public async onShown(winid: number): Promise { if (!this.enabled) return await this.doHighlight(false, debounceInterval, winid) } public async onWinScroll(winid: number): Promise { if (!this.shouldHighlight) return this.cancel(true) let rangeTokenSource = this.rangeTokenSource = new CancellationTokenSource() let token = rangeTokenSource.token await waitWithToken(debounceInterval, token) if (token.isCancellationRequested) return if (this.shouldRangeHighlight) { await this.doRangeHighlight(winid, undefined, token) } else { await this.highlightRegions(winid, token) } } /** * Highlight the span without check regions */ public async onCursorHold(winid: number, lnum: number): Promise { if (!this.enabled) return this.cancel(true) let rangeTokenSource = this.rangeTokenSource = new CancellationTokenSource() let token = rangeTokenSource.token let height = workspace.env.lines let span: [number, number] = [Math.max(0, lnum - height), Math.min(this.doc.lineCount, lnum + height)] if (this.shouldRangeHighlight) { await this.doRangeHighlight(winid, span, token) } else if (this.highlights) { await this.addHighlights(this.highlights, span, token) } } public get hasProvider(): boolean { return languages.hasProvider(ProviderName.SemanticTokens, this.doc) || languages.hasProvider(ProviderName.SemanticTokensRange, this.doc) } private get hasLegend(): boolean { let { textDocument } = this.doc return languages.getLegend(textDocument) != null || languages.getLegend(textDocument, true) != null } public get rangeProviderOnly(): boolean { return !languages.hasProvider(ProviderName.SemanticTokens, this.doc) && languages.hasProvider(ProviderName.SemanticTokensRange, this.doc) } public get shouldRangeHighlight(): boolean { let { textDocument } = this.doc return languages.hasProvider(ProviderName.SemanticTokensRange, textDocument) && this.previousResults == null } /** * Get current highlight items */ public get highlights(): SemanticTokenRange[] | undefined { if (!this._highlights) return undefined return this._highlights[1] } private get buffer(): Buffer { return this.nvim.createBuffer(this.bufnr) } public get enabled(): boolean { if (!this.configEnabled || !this.hasLegend) return false return this.hasProvider } private get shouldHighlight(): boolean { if (!this.enabled) return false const { doc } = this if (doc.dirty || doc.version === this._version) return false return true } public checkState(): void { if (!this.configEnabled) throw new Error(`Semantic tokens highlight not enabled for current filetype: ${this.doc.filetype}`) if (!this.hasProvider || !this.hasLegend) throw new Error(`SemanticTokens provider not found for ${this.doc.uri}`) } public async getTokenRanges( tokens: number[], legend: SemanticTokensLegend, token: CancellationToken): Promise { let currentLine = 0 let currentCharacter = 0 let highlights: SemanticTokenRange[] = [] let toBytes: (characterIndex: number) => number | undefined let textDocument = this.doc.textDocument let tickStart = Date.now() for (let i = 0; i < tokens.length; i += 5) { if (i == 0 || Date.now() - tickStart > yieldEveryMilliseconds) { await waitImmediate() if (token.isCancellationRequested) break tickStart = Date.now() } const deltaLine = tokens[i] const deltaCharacter = tokens[i + 1] const length = tokens[i + 2] const tokenType = legend.tokenTypes[tokens[i + 3]] const tokenModifiers = legend.tokenModifiers.filter((_, m) => tokens[i + 4] & (1 << m)) const lnum = currentLine + deltaLine if (deltaLine != 0 || !toBytes) { toBytes = bytes(toText(textDocument.lines[lnum])) } const sc = deltaLine === 0 ? currentCharacter + deltaCharacter : deltaCharacter const ec = sc + length currentLine = lnum currentCharacter = sc this.addHighlightItems(highlights, [lnum, toBytes(sc), toBytes(ec)], tokenType, tokenModifiers) } if (token.isCancellationRequested) return null return highlights } /** * Single line only. */ private addHighlightItems(highlights: SemanticTokenRange[], range: [number, number, number], tokenType: string, tokenModifiers: string[]): void { // highlight groups: // CocSem + Type + type // CocSem + TypeMod + type + modifier let { combinedModifiers } = this.config let combine = false highlights.push({ range, tokenType, combine, hlGroup: HLGROUP_PREFIX + 'Type' + toHighlightPart(tokenType), tokenModifiers, }) if (tokenModifiers.length) { // only use first modifier to avoid highlight flicking const modifier = tokenModifiers[0] combine = combinedModifiers.includes(modifier) highlights.push({ range, tokenType, combine, hlGroup: HLGROUP_PREFIX + 'TypeMod' + toHighlightPart(tokenType) + toHighlightPart(modifier), tokenModifiers, }) } } private toHighlightItems(highlights: ReadonlyArray, span?: [number, number]): HighlightItem[] { let { incrementTypes } = this.config let filter = Array.isArray(span) let res: HighlightItem[] = [] for (let hi of highlights) { if (!hi.hlGroup) continue let lnum = hi.range[0] if (filter && (lnum < span[0] || lnum > span[1])) continue let item: HighlightItem = { lnum, hlGroup: hi.hlGroup, colStart: hi.range[1], colEnd: hi.range[2], combine: hi.combine } if (incrementTypes.includes(hi.tokenType)) { item.end_incl = true item.start_incl = true } res.push(item) } return res } public async doHighlight(forceFull = false, wait: number = debounceInterval, winid?: number): Promise { this.cancel() let tokenSource = this.tokenSource = new CancellationTokenSource() let token = tokenSource.token await waitWithToken(wait, token) if (token.isCancellationRequested) return const winids = winid == null ? workspace.editors.getBufWinids(this.bufnr) : [winid] if (!this.enabled || winids.length === 0) return if (this.shouldRangeHighlight) { this.cancel(true) let rangeTokenSource = this.rangeTokenSource = new CancellationTokenSource() let rangeToken = rangeTokenSource.token for (const win of winids) { await this.doRangeHighlight(win, undefined, rangeToken) if (rangeToken.isCancellationRequested) break } } if (token.isCancellationRequested || this.rangeProviderOnly) return this.cancel(true) const { doc } = this const version = doc.version let tokenRanges: SemanticTokenRange[] | undefined // TextDocument not changed, need perform highlight since lines possible replaced. if (version === this.previousResults?.version) { if (this._highlights && this._highlights[0] == version) { tokenRanges = this._highlights[1] } else { // possible cancelled. const tokens = this.previousResults.tokens const legend = languages.getLegend(doc.textDocument) tokenRanges = await this.getTokenRanges(tokens, legend, token) } } else { tokenRanges = await this.sendRequest(() => { return this.requestAllHighlights(token, forceFull) }, token) } // request cancelled or can't work if (token.isCancellationRequested || !tokenRanges) return this._highlights = [version, tokenRanges] // Full highlight when no highlight added before or token length < 500 if (!this._dirty || tokenRanges.length < 500) { let succeed = await this.addHighlights(tokenRanges, undefined, token) if (succeed) this._version = version } else { this.regions.clear() await this.highlightRegions(winid, token) } this._onDidRefresh.fire() } private async addHighlights(highlights: ReadonlyArray, span: [number, number] | undefined, token: CancellationToken): Promise { const { bufnr, regions, doc, config } = this let items = this.toHighlightItems(highlights, span) let diff = await window.diffHighlights(bufnr, NAMESPACE, items, span, token) if (!diff || token.isCancellationRequested) return false const priority = config.highlightPriority await window.applyDiffHighlights(bufnr, NAMESPACE, priority, diff, true) this._dirty = true if (span) { regions.add(span[0], span[1]) } else { regions.add(0, doc.lineCount) } return true } private async sendRequest(fn: () => Promise, token: CancellationToken): Promise { try { return await fn() } catch (e) { if (!token.isCancellationRequested) { // Retry on server cancel if (e instanceof CancellationError) { this.doHighlight(true, requestDelay).catch(onUnexpectedError) } else { logger.error('Error on request semanticTokens: ', e) } } return undefined } } /** * Perform range highlight request and update. */ public async doRangeHighlight(winid: number, span: [number, number] | undefined, token: CancellationToken): Promise { const { version } = this.doc let res = await this.sendRequest(() => { return this.requestRangeHighlights(winid, span, token) }, token) if (res == null || token.isCancellationRequested) return const { highlights, start, end } = res if (this.rangeProviderOnly || !this.previousResults) { if (!this._highlights || version !== this._highlights[0]) { this._highlights = [version, []] } let tokenRanges = this._highlights[1] let usedLines: Set = tokenRanges.reduce((p, c) => p.add(c.range[0]), new Set()) highlights.forEach(hi => { if (!usedLines.has(hi.range[0])) { tokenRanges.push(hi) } }) } await this.addHighlights(highlights, [start, end], token) } /** * highlight current visible regions, highlight all associated winids when winid is undefined */ public async highlightRegions(winid: number | undefined, token: CancellationToken): Promise { let { regions, highlights, doc, bufnr } = this if (!highlights) return let spans = await window.getVisibleRanges(bufnr, winid) if (token.isCancellationRequested) return for (let lines of spans) { let span = regions.toUncoveredSpan([lines[0] - 1, lines[1] - 1], workspace.env.lines, doc.lineCount) if (span) await this.addHighlights(highlights, span, token) } } /** * Request highlights for visible range of winid. */ public async requestRangeHighlights(winid: number, span: [number, number] | undefined, token: CancellationToken): Promise { let { nvim, doc } = this if (!span) { let region = await nvim.call('coc#window#visible_range', [winid]) as [number, number] if (!region || token.isCancellationRequested) return null // convert to 0 based span = this.regions.toUncoveredSpan([region[0] - 1, region[1] - 1], workspace.env.lines, doc.lineCount) } if (!span) return null const startLine = span[0] const endLine = span[1] let range = doc.textDocument.intersectWith(Range.create(startLine, 0, endLine + 1, 0)) let res = await languages.provideDocumentRangeSemanticTokens(doc.textDocument, range, token) if (!res || !SemanticTokens.is(res) || token.isCancellationRequested) return null let legend = languages.getLegend(doc.textDocument, true) let highlights = await this.getTokenRanges(res.data, legend, token) if (!highlights) return null return { highlights, start: startLine, end: endLine } } /** * Request highlights from provider, return undefined when can't request or request cancelled * Use range provider only when not semanticTokens provider exists. */ public async requestAllHighlights(token: CancellationToken, forceFull: boolean): Promise { const textDocument = this.doc.textDocument const legend = languages.getLegend(textDocument) const hasEditProvider = languages.hasSemanticTokensEdits(textDocument) const previousResult = forceFull ? null : this.previousResults const version = textDocument.version let result: SemanticTokens | SemanticTokensDelta if (hasEditProvider && previousResult?.resultId) { result = await languages.provideDocumentSemanticTokensEdits(textDocument, previousResult.resultId, token) } else { result = await languages.provideDocumentSemanticTokens(textDocument, token) } if (token.isCancellationRequested || result == null) return let tokens: uinteger[] = [] if (SemanticTokens.is(result)) { tokens = result.data } else if (previousResult && Array.isArray(result.edits)) { tokens = previousResult.tokens result.edits.forEach(e => { tokens.splice(e.start, toNumber(e.deleteCount), ...toArray(e.data)) }) } this.previousResults = { resultId: result.resultId, tokens, version } return await this.getTokenRanges(tokens, legend, token) } public clearHighlight(): void { this.reset() this.buffer.clearNamespace(NAMESPACE) } public onProviderChange(): void { if (!this.hasProvider) { this.cancel() this.clearHighlight() } else { this.reset() this.doHighlight(true, 0).catch(onUnexpectedError) } } public cancel(rangeOnly = false): void { if (this.rangeTokenSource) { this.rangeTokenSource.cancel() this.rangeTokenSource = null } if (rangeOnly) return this.regions.clear() if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } } private reset(): void { this.previousResults = undefined this._highlights = undefined this._version = undefined this.regions.clear() } public dispose(): void { this.cancel() this.reset() this._onDidRefresh.dispose() } } export function toHighlightPart(token: string): string { if (!token) return '' if (highlightGroupMap.has(token)) return highlightGroupMap.get(token) let chars: string[] = [] for (let i = 0; i < token.length; i++) { let ch = token[i] ch = isHighlightGroupCharCode(ch.charCodeAt(0)) ? ch : '_' chars.push(i == 0 ? ch.toUpperCase() : ch) } let part = chars.join('') highlightGroupMap.set(token, part) return part } ================================================ FILE: src/handler/semanticTokens/index.ts ================================================ 'use strict' import type { Neovim } from '@chemzqm/neovim' import commands from '../../commands' import events from '../../events' import languages from '../../languages' import BufferSync from '../../model/bufferSync' import Highlighter from '../../model/highlighter' import { Documentation, FloatFactory } from '../../types' import { disposeAll } from '../../util' import { distinct, toArray } from '../../util/array' import type { Disposable } from '../../util/protocol' import { toErrorText, toText } from '../../util/string' import window from '../../window' import workspace from '../../workspace' import SemanticTokensBuffer, { HLGROUP_PREFIX, NAMESPACE, StaticConfig, toHighlightPart } from './buffer' const headGroup = 'Statement' let floatFactory: FloatFactory | undefined export default class SemanticTokens { private disposables: Disposable[] = [] private highlighters: BufferSync public staticConfig: StaticConfig constructor(private nvim: Neovim) { this.setStaticConfiguration() workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('semanticTokens')) { this.setStaticConfiguration() for (let item of this.highlighters.items) { item.loadConfiguration() } } }, this, this.disposables) commands.register({ id: 'semanticTokens.checkCurrent', execute: async () => { await this.showHighlightInfo() } }, false, 'show semantic tokens highlight information of current buffer') commands.register({ id: 'semanticTokens.refreshCurrent', execute: () => { return this.highlightCurrent() } }, false, 'refresh semantic tokens highlight of current buffer.') commands.register({ id: 'semanticTokens.inspect', execute: () => { return this.inspectSemanticToken() } }, false, 'Inspect semantic token information at cursor position.') commands.register({ id: 'semanticTokens.clearCurrent', execute: async () => { let buf = await nvim.buffer buf.clearNamespace(NAMESPACE, 0, -1) } }, false, 'clear semantic tokens highlight of current buffer') commands.register({ id: 'semanticTokens.clearAll', execute: async () => { let bufs = await nvim.buffers for (let buf of bufs) { buf.clearNamespace(NAMESPACE, 0, -1) } } }, false, 'clear semantic tokens highlight of all buffers') this.highlighters = workspace.registerBufferSync(doc => { return new SemanticTokensBuffer(this.nvim, doc, this.staticConfig) }) languages.onDidSemanticTokensRefresh(async selector => { let visibleBufs = window.visibleTextEditors.map(o => o.document.bufnr) for (let item of this.highlighters.items) { if (workspace.match(selector, item.doc) && visibleBufs.includes(item.bufnr)) { item.onProviderChange() } } }, null, this.disposables) events.on('BufWinEnter', async (bufnr: number, winid: number) => { let item = this.highlighters.getItem(bufnr) if (item) await item.onShown(winid) }, null, this.disposables) events.on('WinScrolled', async (winid: number, bufnr: number) => { let item = this.highlighters.getItem(bufnr) if (item) await item.onWinScroll(winid) }, null, this.disposables) events.on('CursorHold', async (bufnr: number, cursor: [number, number], winid: number) => { let item = this.highlighters.getItem(bufnr) if (item && winid) await item.onCursorHold(winid, cursor[0]) }, null, this.disposables) } public setStaticConfiguration(): void { const filetypes = workspace.initialConfiguration.get('semanticTokens.filetypes', null) this.staticConfig = Object.assign(this.staticConfig ?? {}, { filetypes }) } public async inspectSemanticToken(): Promise { let item = await this.getCurrentItem() if (!item || !item.enabled) { if (!item) { let doc = await workspace.document void window.showErrorMessage(`Document not attached, ${doc.notAttachReason}`) } else { try { item.checkState() } catch (e) { void window.showErrorMessage((e as Error).message) } } this.closeFloat() return } let [_, line, col] = await this.nvim.call('getcurpos', []) as [number, number, number] let highlights = toArray(item.highlights) let highlight = highlights.find(o => { let column = col - 1 return o.range[0] === line - 1 && column >= o.range[1] && column < o.range[2] }) if (highlight) { let modifiers = toArray(highlight.tokenModifiers) let highlights = [] if (highlight.hlGroup) { let s = 'Highlight group: '.length highlights.push({ lnum: 2, colStart: s, colEnd: s + highlight.hlGroup.length, hlGroup: highlight.hlGroup }) } let docs: Documentation[] = [{ filetype: 'txt', content: `Type: ${highlight.tokenType}\nModifiers: ${modifiers.join(', ')}\nHighlight group: ${toText(highlight.hlGroup)}`, highlights }] if (!floatFactory) { floatFactory = window.createFloatFactory({ title: 'Semantic token info', highlight: 'Normal', borderhighlight: 'MoreMsg', border: [1, 1, 1, 1] }) } await floatFactory.show(docs, { winblend: 0 }) } else { this.closeFloat() } } public closeFloat(): void { floatFactory?.close() } public async getCurrentItem(): Promise { let buf = await this.nvim.buffer return this.getItem(buf.id) } public getItem(bufnr: number): SemanticTokensBuffer | undefined { return this.highlighters.getItem(bufnr) } /** * Force highlight of current buffer */ public async highlightCurrent(): Promise { let item = await this.getCurrentItem() if (!item || !item.enabled) throw new Error(`Unable to perform semantic highlights for current buffer.`) await item.forceHighlight() } /** * Show semantic highlight info in temporarily buffer */ public async showHighlightInfo(): Promise { let bufnr = await this.nvim.call('bufnr', ['%']) as number workspace.getAttachedDocument(bufnr) let { nvim } = this let item = this.highlighters.getItem(bufnr) let hl = new Highlighter() nvim.pauseNotification() nvim.command(`vs +setl\\ buftype=nofile __coc_semantic_highlights_${bufnr}__`, true) nvim.command(`setl bufhidden=wipe noswapfile nobuflisted wrap undolevels=-1`, true) nvim.call('bufnr', ['%'], true) let res = await nvim.resumeNotification() hl.addLine('Semantic highlights info', headGroup) hl.addLine('') try { item.checkState() let highlights = item.highlights ?? [] hl.addLine('The number of semantic tokens: ') hl.addText(String(highlights.length), 'Number') hl.addLine('') hl.addLine('Semantic highlight groups used by current buffer', headGroup) hl.addLine('') const groups = distinct(highlights.filter(o => o.hlGroup != null).map(({ hlGroup }) => hlGroup)) for (const hlGroup of groups) { hl.addTexts([{ text: '-', hlGroup: 'Comment' }, { text: ' ' }, { text: hlGroup, hlGroup }]) } hl.addLine('') hl.addLine('Tokens types that current Language Server supported:', headGroup) hl.addLine('') let doc = workspace.getDocument(item.bufnr) let legend = languages.getLegend(doc.textDocument) ?? languages.getLegend(doc.textDocument, true) if (legend.tokenTypes.length) { for (const t of [...new Set(legend.tokenTypes)]) { let text = HLGROUP_PREFIX + 'Type' + toHighlightPart(t) hl.addTexts([{ text: '-', hlGroup: 'Comment' }, { text: ' ' }, { text, hlGroup: text }]) } hl.addLine('') } else { hl.addLine('No token types supported', 'Comment') hl.addLine('') } // modifiers are added to one token type, we can't list them directly } catch (e) { hl.addLine(toErrorText(e)) } nvim.pauseNotification() hl.render(nvim.createBuffer(res[0][2] as number)) nvim.resumeNotification(true, true) } public dispose(): void { this.highlighters.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/handler/signature.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { MarkupContent, Position, SignatureHelp } from 'vscode-languageserver-types' import { IConfigurationChangeEvent } from '../configuration/types' import events from '../events' import languages, { ProviderName } from '../languages' import Document from '../model/document' import { FloatConfig, FloatFactory } from '../types' import { disposeAll, getConditionValue, wait } from '../util' import { isFalsyOrEmpty } from '../util/array' import { debounce } from '../util/node' import { CancellationTokenSource, Disposable, SignatureHelpTriggerKind } from '../util/protocol' import { byteLength, byteSlice } from '../util/string' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' import { toDocumentation } from './util' interface SignatureConfig { wait: number enableTrigger: boolean target: string preferAbove: boolean hideOnChange: boolean floatConfig: FloatConfig } interface SignaturePosition { bufnr: number lnum: number col: number } interface SignaturePart { text: string type: 'Label' | 'MoreMsg' | 'Normal' } const debounceTime = getConditionValue(100, 10) export default class Signature { private timer: NodeJS.Timeout private config: SignatureConfig private signatureFactory: FloatFactory private lastPosition: SignaturePosition | undefined private disposables: Disposable[] = [] private tokenSource: CancellationTokenSource | undefined constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.loadConfiguration() this.signatureFactory = window.createFloatFactory(Object.assign({ preferTop: this.config.preferAbove, autoHide: false, modes: ['i', 'ic', 's'], }, this.config.floatConfig)) this.disposables.push(this.signatureFactory) workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) events.on('CursorMovedI', debounce(this.checkCursor.bind(this), debounceTime), null, this.disposables) events.on('BufEnter', () => { this.tokenSource?.cancel() }, null, this.disposables) events.on('TextChangedI', () => { if (this.config.hideOnChange) { this.signatureFactory.close() } }, null, this.disposables) events.on('TextInsert', async (bufnr, info, character) => { if (!this.shouldAutoTrigger(bufnr, character)) return let doc = workspace.getDocument(bufnr) await this._triggerSignatureHelp(doc, { line: info.lnum - 1, character: info.pre.length }, false) }, null, this.disposables) events.on('PlaceholderJump', async (bufnr, info) => { if (workspace.env.jumpAutocmd || info.charbefore === '') return // need wait for CursorMoved events on placeholder select await wait(50) if (!this.shouldAutoTrigger(bufnr, info.charbefore)) return let doc = workspace.getDocument(bufnr) await this._triggerSignatureHelp(doc, info.range.start, false) }, null, this.disposables) window.onDidChangeActiveTextEditor(() => { this.loadConfiguration() }, null, this.disposables) } public shouldAutoTrigger(bufnr: number, character: string): boolean { if (!this.config.enableTrigger) return false let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached || !languages.shouldTriggerSignatureHelp(doc.textDocument, character)) return false return true } private checkCursor(bufnr: number, cursor: [number, number]): void { let pos = this.lastPosition let floatFactory = this.signatureFactory if (!pos || bufnr !== pos.bufnr || floatFactory.window == null) return let doc = workspace.getDocument(bufnr) if (!doc || cursor[0] != pos.lnum || cursor[1] < pos.col) { floatFactory.close() return } let line = doc.getline(pos.lnum - 1) let text = byteSlice(line, pos.col - 1, cursor[1] - 1) if (text.endsWith(')')) return floatFactory.close() } public loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('signature')) { let doc = window.activeTextEditor?.document let config = workspace.getConfiguration('signature', doc) this.config = { target: config.get('target', 'float'), floatConfig: config.get('floatConfig', {}), enableTrigger: config.get('enable', true), wait: Math.max(config.get('triggerSignatureWait', 500), 200), preferAbove: config.get('preferShownAbove', true), hideOnChange: config.get('hideOnTextChange', false), } } } public async triggerSignatureHelp(): Promise { let { doc, position } = await this.handler.getCurrentState() if (!languages.hasProvider(ProviderName.Signature, doc.textDocument)) return false return await this._triggerSignatureHelp(doc, position, true, 0) } private async _triggerSignatureHelp(doc: Document, position: Position, invoke: boolean, offset = 0): Promise { this.tokenSource?.cancel() let tokenSource = this.tokenSource = new CancellationTokenSource() let token = tokenSource.token token.onCancellationRequested(() => { tokenSource.dispose() this.tokenSource = undefined }) let { target } = this.config let timer = this.timer = setTimeout(() => { tokenSource.cancel() }, this.config.wait) await doc.patchChange() let signatureHelp = await languages.getSignatureHelp(doc.textDocument, position, token, { isRetrigger: this.signatureFactory.checkRetrigger(doc.bufnr), triggerKind: invoke ? SignatureHelpTriggerKind.Invoked : SignatureHelpTriggerKind.TriggerCharacter }) clearTimeout(timer) if (token.isCancellationRequested) return false if (!signatureHelp || signatureHelp.signatures.length == 0) { this.signatureFactory.close() return false } let { activeSignature, signatures } = signatureHelp if (activeSignature) { // make active first let [active] = signatures.splice(activeSignature, 1) if (active) signatures.unshift(active) } if (target == 'echo') { this.echoSignature(signatureHelp) } else { await this.showSignatureHelp(doc, position, signatureHelp, offset) } return true } private async showSignatureHelp(doc: Document, position: Position, signatureHelp: SignatureHelp, offset: number): Promise { let { signatures, activeParameter } = signatureHelp activeParameter = typeof activeParameter === 'number' ? activeParameter : undefined let paramDoc: string | MarkupContent = null let startOffset = offset let docs = signatures.reduce((p, c, idx) => { let activeIndexes: [number, number] = null let activeIndex = c.activeParameter ?? activeParameter if (activeIndex === undefined && !isFalsyOrEmpty(c.parameters)) { activeIndex = 0 } let nameIndex = c.label.indexOf('(') if (idx == 0 && typeof activeIndex === 'number') { let active = c.parameters?.[activeIndex] if (active) { let after = c.label.slice(nameIndex == -1 ? 0 : nameIndex) paramDoc = active.documentation if (typeof active.label === 'string') { let str = after.slice(0) let ms = str.match(new RegExp('\\b' + active.label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b')) let index = ms ? ms.index : str.indexOf(active.label) if (index != -1) { activeIndexes = [ index + nameIndex, index + active.label.length + nameIndex ] } } else { activeIndexes = active.label } } } if (activeIndexes == null) { activeIndexes = [nameIndex + 1, nameIndex + 1] } if (offset == startOffset) { offset = offset + activeIndexes[0] + 1 } p.push({ content: c.label, filetype: doc.filetype, active: activeIndexes }) if (paramDoc) { p.push(toDocumentation(paramDoc)) } if (idx == 0 && c.documentation) { p.push(toDocumentation(c.documentation)) } return p }, []) let content = doc.getline(position.line, false).slice(0, position.character) this.lastPosition = { bufnr: doc.bufnr, lnum: position.line + 1, col: byteLength(content) + 1 } await this.signatureFactory.show(docs, { offsetX: offset }) } public echoSignature(signatureHelp: SignatureHelp): void { let { signatures, activeParameter } = signatureHelp let columns = workspace.env.columns signatures = signatures.slice(0, workspace.env.cmdheight) let signatureList: SignaturePart[][] = [] for (let signature of signatures) { let parts: SignaturePart[] = [] let { label } = signature label = label.replace(/\n/g, ' ') if (label.length >= columns - 16) { label = label.slice(0, columns - 16) + '...' } let nameIndex = label.indexOf('(') if (nameIndex == -1) { parts = [{ text: label, type: 'Normal' }] } else { parts.push({ text: label.slice(0, nameIndex), type: 'Label' }) let after = label.slice(nameIndex) if (signatureList.length == 0 && activeParameter != null) { let active = signature.parameters?.[activeParameter] if (active) { let start: number let end: number if (typeof active.label === 'string') { let str = after.slice(0) let ms = str.match(new RegExp('\\b' + active.label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b')) let idx = ms ? ms.index : str.indexOf(active.label) if (idx == -1) { parts.push({ text: after, type: 'Normal' }) } else { start = idx end = idx + active.label.length } } else { [start, end] = active.label start = start - nameIndex end = end - nameIndex } if (start != null && end != null) { parts.push({ text: after.slice(0, start), type: 'Normal' }) parts.push({ text: after.slice(start, end), type: 'MoreMsg' }) parts.push({ text: after.slice(end), type: 'Normal' }) } } } else { parts.push({ text: after, type: 'Normal' }) } } signatureList.push(parts) } this.nvim.callTimer('coc#ui#echo_signatures', [signatureList], true) } public dispose(): void { disposeAll(this.disposables) if (this.timer) { clearTimeout(this.timer) } } } ================================================ FILE: src/handler/symbols/buffer.ts ================================================ 'use strict' import { DocumentSymbol } from 'vscode-languageserver-types' import languages from '../../languages' import { createLogger } from '../../logger' import { SyncItem } from '../../model/bufferSync' import Document from '../../model/document' import { DidChangeTextDocumentParams } from '../../types' import { disposeAll, getConditionValue } from '../../util' import { onUnexpectedError } from '../../util/errors' import { debounce } from '../../util/node' import { CancellationTokenSource, Disposable, Emitter, Event } from '../../util/protocol' const logger = createLogger('symbols-buffer') const DEBEBOUNCE_INTERVAL = getConditionValue(500, 10) export default class SymbolsBuffer implements SyncItem { private disposables: Disposable[] = [] public fetchSymbols: (() => void) & { clear(): void } private version: number public symbols: DocumentSymbol[] private tokenSource: CancellationTokenSource private readonly _onDidUpdate = new Emitter() public readonly onDidUpdate: Event = this._onDidUpdate.event constructor(public readonly doc: Document, private autoUpdateBufnrs: Set) { this.fetchSymbols = debounce(() => { this._fetchSymbols().catch(onUnexpectedError) }, DEBEBOUNCE_INTERVAL) } /** * Enable autoUpdate when invoked. */ public async getSymbols(): Promise { let { doc } = this await doc.patchChange() this.autoUpdateBufnrs.add(doc.bufnr) // refresh for empty symbols since some languages server could be buggy first time. if (doc.version == this.version && this.symbols?.length) return this.symbols this.cancel() await this._fetchSymbols() return this.symbols } public onChange(e: DidChangeTextDocumentParams): void { if (e.contentChanges.length === 0) return this.cancel() if (this.autoUpdateBufnrs.has(this.doc.bufnr)) { this.fetchSymbols() } } private async _fetchSymbols(): Promise { let { textDocument } = this.doc let { version } = textDocument let tokenSource = this.tokenSource = new CancellationTokenSource() let { token } = tokenSource let symbols = await languages.getDocumentSymbol(textDocument, token) this.tokenSource = undefined if (symbols == null || token.isCancellationRequested) return this.version = version this.symbols = symbols this._onDidUpdate.fire(symbols) } public cancel(): void { this.fetchSymbols.clear() if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource.dispose() this.tokenSource = null } } public dispose(): void { this.cancel() this.symbols = undefined this._onDidUpdate.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/handler/symbols/index.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Position, Range, WorkspaceSymbol } from 'vscode-languageserver-types' import events from '../../events' import languages, { ProviderName } from '../../languages' import BufferSync from '../../model/bufferSync' import { disposeAll, getConditionValue } from '../../util/index' import { debounce } from '../../util/node' import { equals } from '../../util/object' import { positionInRange, rangeInRange } from '../../util/position' import { CancellationTokenSource, Disposable } from '../../util/protocol' import { characterIndex } from '../../util/string' import window from '../../window' import workspace from '../../workspace' import { HandlerDelegate } from '../types' import SymbolsBuffer from './buffer' import Outline from './outline' import { convertSymbols, SymbolInfo } from './util' export default class Symbols { private buffers: BufferSync private disposables: Disposable[] = [] private outline: Outline private autoUpdateBufnrs: Set = new Set() constructor( private nvim: Neovim, private handler: HandlerDelegate ) { this.buffers = workspace.registerBufferSync(doc => { let { bufnr } = doc let buf = new SymbolsBuffer(doc, this.autoUpdateBufnrs) buf.onDidUpdate(symbols => { if (!this.outline) return this.outline.onSymbolsUpdate(bufnr, symbols) }) return buf }) this.outline = new Outline(nvim, this.buffers, handler) const debounceTime = workspace.initialConfiguration.get('coc.preferences.currentFunctionSymbolDebounceTime', 300) let prev = '' let debounced = debounce(async (bufnr: number, cursor: [number, number]) => { if (!this.buffers.getItem(bufnr) || !this.autoUpdate(bufnr)) return let doc = workspace.getDocument(bufnr) let character = characterIndex(doc.getline(cursor[0] - 1), cursor[1] - 1) let pos = Position.create(cursor[0] - 1, character) let func = await this.getFunctionSymbol(bufnr, pos) let buffer = nvim.createBuffer(bufnr) if (func != prev) { prev = func buffer.setVar('coc_current_function', func, true) this.nvim.callTimer('coc#util#do_autocmd', ['CocStatusChange'], true) } }, getConditionValue(debounceTime, 0)) events.on('CursorMoved', debounced, this, this.disposables) this.disposables.push(Disposable.create(() => { debounced.clear() })) events.on('InsertEnter', (bufnr: number) => { debounced.clear() let buf = this.buffers.getItem(bufnr) if (buf) buf.cancel() }, null, this.disposables) } public autoUpdate(bufnr: number): boolean { let doc = workspace.getDocument(bufnr) let config = workspace.getConfiguration('coc.preferences', doc) return config.get('currentFunctionSymbolAutoUpdate', false) } public get labels(): { [key: string]: string } { return workspace.getConfiguration('suggest').get('completionItemKindLabels', {}) } public async getWorkspaceSymbols(input: string): Promise { this.handler.checkProvider(ProviderName.WorkspaceSymbols, null) let tokenSource = new CancellationTokenSource() return await languages.getWorkspaceSymbols(input, tokenSource.token) } public async resolveWorkspaceSymbol(symbolInfo: WorkspaceSymbol): Promise { if (symbolInfo.location?.uri) return symbolInfo let tokenSource = new CancellationTokenSource() return await languages.resolveWorkspaceSymbol(symbolInfo, tokenSource.token) } public async getDocumentSymbols(bufnr?: number): Promise { if (!bufnr) { bufnr = await this.nvim.call('bufnr', ['%']) as number let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached) return undefined } let buf = this.buffers.getItem(bufnr) if (!buf) return let res = await buf.getSymbols() return res ? convertSymbols(res) : undefined } public async getFunctionSymbol(bufnr: number, position: Position): Promise { let symbols = await this.getDocumentSymbols(bufnr) if (!symbols || symbols.length === 0) return '' symbols = symbols.filter(s => [ 'Class', 'Method', 'Function', 'Struct', ].includes(s.kind)) let functionName = '' let labels = this.labels for (let sym of symbols.reverse()) { if (sym.range && positionInRange(position, sym.range) == 0 && !sym.text.endsWith(') callback')) { functionName = sym.text let label = labels[sym.kind.toLowerCase()] if (label) functionName = `${label} ${functionName}` break } } return functionName } public async getCurrentFunctionSymbol(): Promise { let bufnr = await this.nvim.call('bufnr', ['%']) as number let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached || !languages.hasProvider(ProviderName.DocumentSymbol, doc.textDocument)) return let position = await window.getCursorPosition() return await this.getFunctionSymbol(bufnr, position) } /* * supportedSymbols must be string values of symbolKind */ public async selectSymbolRange(inner: boolean, visualmode: string, supportedSymbols: string[]): Promise { let { doc } = await this.handler.getCurrentState() this.handler.checkProvider(ProviderName.DocumentSymbol, doc.textDocument) let range: Range if (visualmode) { range = await window.getSelectedRange(visualmode) } else { let pos = await window.getCursorPosition() range = Range.create(pos, pos) } let symbols = await this.getDocumentSymbols(doc.bufnr) if (!symbols || symbols.length === 0) { void window.showWarningMessage('No symbols found') return } symbols = symbols.filter(s => supportedSymbols.includes(s.kind)) let selectRange: Range for (let sym of symbols.reverse()) { if (sym.range && !equals(sym.range, range) && rangeInRange(range, sym.range)) { selectRange = sym.range break } } if (inner && selectRange) { let { start, end } = selectRange let line = doc.getline(start.line + 1) // https://github.com/neoclide/coc.nvim/issues/1847 // https://github.com/neoclide/coc.nvim/pull/4488#issuecomment-1409717682 // don't decrease end.line for python let endDelta = doc.filetype === 'python' ? 0 : 1 let endLine = doc.getline(end.line - endDelta) selectRange = Range.create(start.line + 1, line.match(/^\s*/)[0].length, end.line - endDelta, endLine.length) } if (selectRange) { await window.selectRange(selectRange) } else if (['v', 'V', '\x16'].includes(visualmode)) { await this.nvim.command('normal! gv') } } public async showOutline(keep?: number): Promise { await this.outline.show(keep) } public async hideOutline(): Promise { await this.outline.hide() } public hasOutline(bufnr: number): boolean { return this.outline.has(bufnr) } public dispose(): void { this.outline.dispose() this.buffers.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/handler/symbols/outline.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { CodeActionKind, DocumentSymbol, Position, Range, SymbolKind, SymbolTag } from 'vscode-languageserver-types' import type { IConfigurationChangeEvent } from '../../configuration/types' import events from '../../events' import languages, { ProviderName } from '../../languages' import BufferSync from '../../model/bufferSync' import BasicDataProvider, { TreeNode } from '../../tree/BasicDataProvider' import BasicTreeView from '../../tree/TreeView' import { disposeAll, getConditionValue } from '../../util' import { comparePosition, positionInRange } from '../../util/position' import type { Disposable } from '../../util/protocol' import window from '../../window' import workspace from '../../workspace' import { HandlerDelegate } from '../types' import SymbolsBuffer from './buffer' // Support expand level. interface OutlineNode extends TreeNode { kind: SymbolKind range: Range selectRange: Range } interface OutlineConfig { splitCommand: string switchSortKey: string togglePreviewKey: string followCursor: boolean keepWindow: boolean expandLevel: number checkBufferSwitch: boolean showLineNumber: boolean autoWidth: boolean detailAsDescription: boolean codeActionKinds: CodeActionKind[] sortBy: 'position' | 'name' | 'category' autoHide: boolean autoPreview: boolean previewMaxWidth: number previewBorder: boolean previewBorderRounded: boolean previewHighlightGroup: string previewBorderHighlightGroup: string previewWinblend: number } const hoverTimeout = getConditionValue(300, 10) /** * Manage TreeViews and Providers of outline. */ export default class SymbolsOutline { private treeViewList: BasicTreeView[] = [] private providersMap: Map> = new Map() private sortByMap: Map = new Map() private config: OutlineConfig private disposables: Disposable[] = [] constructor( private nvim: Neovim, private buffers: BufferSync, private handler: HandlerDelegate ) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) workspace.onDidCloseTextDocument(async e => { let { bufnr } = e let provider = this.providersMap.get(bufnr) if (!provider) return let loaded = await nvim.call('bufloaded', [bufnr]) as number // reload detected if (loaded) return this.providersMap.delete(bufnr) provider.dispose() }, null, this.disposables) window.onDidChangeActiveTextEditor(async editor => { if (!this.config.checkBufferSwitch) return let view = this.treeViewList.find(v => v.visible && v.targetTabId == editor.tabpageid) if (view) { await this.showOutline(editor.document.bufnr, editor.tabpageid) await nvim.command(`noa call win_gotoid(${editor.winid})`) } }, null, this.disposables) events.on('CursorHold', async (bufnr: number, cursor: [number, number]) => { if (!this.config.followCursor) return let provider = this.providersMap.get(bufnr) if (!provider) return let tabpage = await nvim.tabpage let view = this.treeViewList.find(o => o.visible && o.targetBufnr == bufnr && o.targetTabId == tabpage.id) if (!view) return await this.revealPosition(bufnr, view, Position.create(cursor[0] - 1, cursor[1] - 1)) }, null, this.disposables) } private async revealPosition(bufnr: number, treeView: BasicTreeView, position: Position): Promise { let provider = this.providersMap.get(bufnr) let nodes = await Promise.resolve(provider.getChildren()) let curr = getNodeByPosition(position, nodes) if (curr) await treeView.reveal(curr) } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('outline')) { let c = workspace.getConfiguration('outline', null) this.config = { splitCommand: c.get('splitCommand'), switchSortKey: c.get('switchSortKey'), togglePreviewKey: c.get('togglePreviewKey'), followCursor: c.get('followCursor'), keepWindow: c.get('keepWindow'), expandLevel: c.get('expandLevel'), autoWidth: c.get('autoWidth'), checkBufferSwitch: c.get('checkBufferSwitch'), detailAsDescription: c.get('detailAsDescription'), sortBy: c.get<'position' | 'name' | 'category'>('sortBy'), showLineNumber: c.get('showLineNumber'), codeActionKinds: c.get('codeActionKinds'), autoHide: c.get('autoHide'), autoPreview: c.get('autoPreview'), previewMaxWidth: c.get('previewMaxWidth'), previewBorder: c.get('previewBorder'), previewBorderRounded: c.get('previewBorderRounded'), previewHighlightGroup: c.get('previewHighlightGroup'), previewBorderHighlightGroup: c.get('previewBorderHighlightGroup'), previewWinblend: c.get('previewWinblend'), } } } private convertSymbolToNode(documentSymbol: DocumentSymbol, sortFn: (a: OutlineNode, b: OutlineNode) => number): OutlineNode { let descs = [] let { detailAsDescription, showLineNumber } = this.config if (detailAsDescription && documentSymbol.detail) descs.push(documentSymbol.detail) if (showLineNumber) descs.push(`${documentSymbol.selectionRange.start.line + 1}`) return { label: documentSymbol.name, tooltip: detailAsDescription ? undefined : documentSymbol.detail, description: descs.join(' '), icon: this.handler.getIcon(documentSymbol.kind), deprecated: documentSymbol.tags?.includes(SymbolTag.Deprecated), kind: documentSymbol.kind, range: documentSymbol.range, selectRange: documentSymbol.selectionRange, children: Array.isArray(documentSymbol.children) ? documentSymbol.children.map(o => { return this.convertSymbolToNode(o, sortFn) }).sort(sortFn) : undefined } } private setMessage(bufnr: number, msg: string | undefined): void { this.treeViewList.forEach(v => { if (v.valid && v.targetBufnr == bufnr) { v.message = msg } }) } private convertSymbols(bufnr: number, symbols: DocumentSymbol[]): OutlineNode[] { let sortBy = this.getSortBy(bufnr) let sortFn = (a: OutlineNode, b: OutlineNode): number => { if (sortBy === 'name') { return a.label < b.label ? -1 : 1 } if (sortBy === 'category') { if (a.kind == b.kind) return a.label < b.label ? -1 : 1 return a.kind - b.kind } return comparePosition(a.selectRange.start, b.selectRange.start) } return symbols.map(s => this.convertSymbolToNode(s, sortFn)).sort(sortFn) } public onSymbolsUpdate(bufnr: number, symbols: DocumentSymbol[]): void { let provider = this.providersMap.get(bufnr) if (provider) provider.update(this.convertSymbols(bufnr, symbols)) } private createProvider(bufnr: number): BasicDataProvider { let { nvim } = this let provider = new BasicDataProvider({ expandLevel: this.config.expandLevel, provideData: async () => { let buf = this.buffers.getItem(bufnr) if (!buf) throw new Error('Document not attached') let doc = workspace.getDocument(bufnr) if (!languages.hasProvider(ProviderName.DocumentSymbol, doc.textDocument)) { throw new Error('Document symbol provider not found') } let meta = languages.getDocumentSymbolMetadata(doc.textDocument) if (meta && meta.label) { let views = this.treeViewList.filter(v => v.valid && v.targetBufnr == bufnr) views.forEach(view => view.description = meta.label) } this.setMessage(bufnr, 'Loading document symbols') let arr = await buf.getSymbols() if (!arr || arr.length == 0) { // server may return empty symbols on buffer initialize, throw error to force reload. throw new Error('Empty symbols returned from language server. ') } this.setMessage(bufnr, undefined) return this.convertSymbols(bufnr, arr) }, handleClick: async item => { let winnr = await nvim.call('bufwinnr', [bufnr]) if (winnr == -1) return nvim.pauseNotification() nvim.command(`${winnr}wincmd w`, true) let pos = item.selectRange.start nvim.call('coc#cursor#move_to', [pos.line, pos.character], true) nvim.command(`normal! zz`, true) let buf = nvim.createBuffer(bufnr) buf.highlightRanges('outline-hover', 'CocHoverRange', [item.selectRange]) nvim.command('redraw', true) await nvim.resumeNotification() setTimeout(() => { buf.clearNamespace('outline-hover') nvim.command('redraw', true) }, hoverTimeout) if (this.config.autoHide) { await this.hide() } }, resolveActions: async (_, element) => { let winnr = await nvim.call('bufwinnr', [bufnr]) if (winnr == -1) return let doc = workspace.getDocument(bufnr) let actions = await this.handler.getCodeActions(doc, element.range, this.config.codeActionKinds) let arr = actions.map(o => { return { title: o.title, handler: async () => { let position = element.range.start await nvim.command(`${winnr}wincmd w`) await this.nvim.call('coc#cursor#move_to', [position.line, position.character]) await this.handler.applyCodeAction(o) } } }) return [...arr, { title: 'Visual Select', handler: async item => { await nvim.command(`${winnr}wincmd w`) await window.selectRange(item.range) } }] }, onDispose: () => { for (let view of this.treeViewList.slice()) { if (view.provider === provider) { view.dispose() } } } }) return provider } private getSortBy(bufnr: number): string { return this.sortByMap.get(bufnr) ?? this.config.sortBy } private async showOutline(bufnr: number, tabId: number): Promise> { if (!this.providersMap.has(bufnr)) { this.providersMap.set(bufnr, this.createProvider(bufnr)) } let treeView = this.treeViewList.find(v => v.valid && v.targetBufnr == bufnr && v.targetTabId == tabId) if (!treeView) { let { switchSortKey, togglePreviewKey } = this.config let autoPreview = this.config.autoPreview let previewBufnr: number | undefined treeView = new BasicTreeView('OUTLINE', { autoWidth: this.config.autoWidth, bufhidden: 'hide', enableFilter: true, treeDataProvider: this.providersMap.get(bufnr), }) let sortBy = this.getSortBy(bufnr) let prev: OutlineNode | undefined treeView.description = `${sortBy[0].toUpperCase()}${sortBy.slice(1)}` this.treeViewList.push(treeView) let disposable = events.on('BufEnter', bufnr => { if (previewBufnr && bufnr !== previewBufnr) { prev = undefined this.closePreview() } }) treeView.onDispose(() => { let idx = this.treeViewList.findIndex(v => v === treeView) if (idx !== -1) this.treeViewList.splice(idx, 1) disposable.dispose() this.closePreview() }) treeView.onDidChangeVisibility(visible => { if (this.nvim.isVim && visible && previewBufnr) { prev = undefined this.closePreview() } }) treeView.onDidCursorMoved(async node => { if (autoPreview && prev !== node) { prev = node previewBufnr = await this.doPreview(bufnr, node) } }) treeView.registerLocalKeymap('n', switchSortKey, async () => { let arr = ['category', 'name', 'position'] let curr = this.getSortBy(bufnr) let items = arr.map(s => { return { text: s, disabled: s === curr } }) let res = await window.showMenuPicker(items, { title: 'Choose sort method' }) if (res < 0) return let sortBy = arr[res] this.sortByMap.set(bufnr, sortBy) let views = this.treeViewList.filter(o => o.targetBufnr == bufnr) views.forEach(view => { view.description = `${sortBy[0].toUpperCase()}${sortBy.slice(1)}` }) let item = this.buffers.getItem(bufnr) this.onSymbolsUpdate(bufnr, item.symbols) }, true) treeView.registerLocalKeymap('n', togglePreviewKey, async node => { autoPreview = !autoPreview if (!autoPreview) { prev = undefined this.closePreview() } else { previewBufnr = await this.doPreview(bufnr, node) } }, true) } await treeView.show(this.config.splitCommand, false) return treeView } public async doPreview(bufnr: number, node: OutlineNode | undefined): Promise { if (!node) { this.closePreview() return } let config: any = { bufnr, range: node.range, border: this.config.previewBorder, rounded: this.config.previewBorderRounded, maxWidth: this.config.previewMaxWidth, highlight: this.config.previewHighlightGroup, borderhighlight: this.config.previewBorderHighlightGroup, winblend: this.config.previewWinblend } return await this.nvim.call('coc#ui#outline_preview', [config]) as number } private closePreview(): void { this.nvim.call('coc#ui#outline_close_preview', [], true) } /** * Create outline view. */ public async show(keep?: number): Promise { let [bufnr, winid] = await this.nvim.eval('[bufnr("%"),win_getid()]') as [number, number] let tabpage = await this.nvim.tabpage let doc = workspace.getDocument(bufnr) if (doc && !doc.attached) { void window.showErrorMessage(`Unable to show outline, ${doc.notAttachReason}`) return } let position = await window.getCursorPosition() let treeView = await this.showOutline(bufnr, tabpage.id) if (keep == 1 || (keep === undefined && this.config.keepWindow)) { await this.nvim.command(`noa call win_gotoid(${winid})`) } else if (this.config.followCursor) { let disposable = treeView.onDidRefrash(async () => { disposable.dispose() let curr = await this.nvim.eval('bufnr("%")') if (curr == bufnr && treeView.visible) { await this.revealPosition(bufnr, treeView, position) } }) } } public has(bufnr: number): boolean { return this.providersMap.has(bufnr) } /** * Hide outline of current tab. */ public async hide(): Promise { let winid = await this.nvim.call('coc#window#find', ['cocViewId', 'OUTLINE']) as number if (winid == -1) return await this.nvim.call('coc#window#close', [winid]) } public dispose(): void { for (let view of this.treeViewList) { view.dispose() } this.treeViewList = [] for (let provider of this.providersMap.values()) { provider.dispose() } this.providersMap.clear() disposeAll(this.disposables) } } function getNodeByPosition(position: Position, nodes: ReadonlyArray): OutlineNode | undefined { let curr: OutlineNode | undefined let checkNodes = (nodes: ReadonlyArray): void => { for (let node of nodes) { if (positionInRange(position, node.range) == 0) { curr = node if (Array.isArray(node.children)) { checkNodes(node.children) } break } } } checkNodes(nodes) return curr } ================================================ FILE: src/handler/symbols/util.ts ================================================ 'use strict' import { DocumentSymbol, Range, SymbolTag } from 'vscode-languageserver-types' import { defaultValue } from '../../util' import { getSymbolKind } from '../../util/convert' import { comparePosition } from '../../util/position' export interface SymbolInfo { filepath?: string lnum: number col: number text: string kind: string level?: number detail?: string deprecated?: boolean containerName?: string range: Range selectionRange?: Range } export function convertSymbols(symbols: DocumentSymbol[]): SymbolInfo[] { let res: SymbolInfo[] = [] let arr = symbols.slice() arr.sort(sortDocumentSymbols) arr.forEach(s => addDocumentSymbol(res, s, 0)) return res } function sortDocumentSymbols(a: DocumentSymbol, b: DocumentSymbol): number { let ra = a.selectionRange let rb = b.selectionRange return comparePosition(ra.start, rb.start) } function addDocumentSymbol(res: SymbolInfo[], sym: DocumentSymbol, level: number): void { let { name, selectionRange, detail, kind, children, range, tags } = sym let { start } = defaultValue(selectionRange, range) let obj: SymbolInfo = { col: start.character + 1, lnum: start.line + 1, text: name, level, kind: getSymbolKind(kind), range, selectionRange } if (detail) obj.detail = detail if (tags && tags.includes(SymbolTag.Deprecated)) obj.deprecated = true res.push(obj) if (children && children.length) { children.sort(sortDocumentSymbols) for (let sym of children) { addDocumentSymbol(res, sym, level + 1) } } } ================================================ FILE: src/handler/typeHierarchy.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, TypeHierarchyItem } from 'vscode-languageserver-types' import commands from '../commands' import type { IConfigurationChangeEvent } from '../configuration/types' import events from '../events' import languages, { ProviderName } from '../languages' import { TreeDataProvider } from '../tree/index' import LocationsDataProvider from '../tree/LocationsDataProvider' import BasicTreeView from '../tree/TreeView' import { disposeAll } from '../util' import { isFalsyOrEmpty } from '../util/array' import { omit } from '../util/lodash' import { CancellationToken, Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { HandlerDelegate } from './types' interface TypeHierarchyDataItem extends TypeHierarchyItem { parent?: TypeHierarchyDataItem children?: TypeHierarchyItem[] } interface TypeHierarchyConfig { splitCommand: string openCommand: string enableTooltip: boolean } type TypeHierarchyKind = 'supertypes' | 'subtypes' interface TypeHierarchyProvider extends TreeDataProvider { meta: TypeHierarchyKind dispose: () => void } /** * Cleanup properties used by treeview */ function toTypeHierarchyItem(item: TypeHierarchyDataItem): TypeHierarchyItem { return omit(item, ['children', 'parent']) } export default class TypeHierarchyHandler { private config: TypeHierarchyConfig private disposables: Disposable[] = [] public static rangesHighlight = 'CocSelectedRange' private highlightWinids: Set = new Set() public static commandId = 'typeHierarchy.reveal' constructor(private nvim: Neovim, private handler: HandlerDelegate) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) events.on('BufWinEnter', (_, winid) => { if (this.highlightWinids.has(winid)) { this.highlightWinids.delete(winid) let win = nvim.createWindow(winid) win.clearMatchGroup(TypeHierarchyHandler.rangesHighlight) } }, null, this.disposables) this.disposables.push(commands.registerCommand(TypeHierarchyHandler.commandId, async (winid: number, item: TypeHierarchyDataItem, openCommand?: string) => { let { nvim } = this await nvim.call('win_gotoid', [winid]) await workspace.jumpTo(item.uri, item.range.start, openCommand) let win = await nvim.window win.clearMatchGroup(TypeHierarchyHandler.rangesHighlight) win.highlightRanges(TypeHierarchyHandler.rangesHighlight, [item.selectionRange], 10, true) this.highlightWinids.add(win.id) }, null, true)) } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('typeHierarchy')) { let c = workspace.getConfiguration('typeHierarchy', null) this.config = { splitCommand: c.get('splitCommand'), openCommand: c.get('openCommand'), enableTooltip: c.get('enableTooltip') } } } private createProvider(rootItems: TypeHierarchyDataItem[], winid: number, kind: TypeHierarchyKind): TypeHierarchyProvider { let provider = new LocationsDataProvider( kind, winid, this.config, TypeHierarchyHandler.commandId, rootItems, kind => this.handler.getIcon(kind), (el, meta, token) => this.getChildren(el, meta, token) ) provider.addAction(`Show Super Types`, (el: TypeHierarchyDataItem) => { provider.meta = 'supertypes' let rootItems = [omit(el, ['children', 'parent'])] provider.reset(rootItems) }) provider.addAction(`Show Sub Types`, (el: TypeHierarchyDataItem) => { provider.meta = 'subtypes' let rootItems = [omit(el, ['children', 'parent'])] provider.reset(rootItems) }) return provider } private async getChildren(item: TypeHierarchyDataItem, kind: TypeHierarchyKind, token: CancellationToken): Promise { let res: TypeHierarchyDataItem[] = [] let typeHierarchyItem = toTypeHierarchyItem(item) if (kind == 'supertypes') { res = await languages.provideTypeHierarchySupertypes(typeHierarchyItem, token) } else { res = await languages.provideTypeHierarchySubtypes(typeHierarchyItem, token) } return res } private async prepare(doc: TextDocument, position: Position): Promise { this.handler.checkProvider(ProviderName.TypeHierarchy, doc) return await this.handler.withRequestToken('typeHierarchy', async token => { return await languages.prepareTypeHierarchy(doc, position, token) }, false) } public async showTypeHierarchyTree(kind: TypeHierarchyKind): Promise { const { doc, position, winid } = await this.handler.getCurrentState() await doc.synchronize() const rootItems = await this.prepare(doc.textDocument, position) if (isFalsyOrEmpty(rootItems)) { void window.showWarningMessage('Unable to get TypeHierarchyItems at cursor position.') return } let provider = this.createProvider(rootItems, winid, kind) let treeView = new BasicTreeView('TYPES', { treeDataProvider: provider }) treeView.title = getTitle(kind) provider.onDidChangeTreeData(e => { if (!e) treeView.title = getTitle(provider.meta) }) treeView.onDidChangeVisibility(e => { if (!e.visible) provider.dispose() }) this.disposables.push(treeView) await treeView.show(this.config.splitCommand) } public dispose(): void { this.highlightWinids.clear() disposeAll(this.disposables) } } function getTitle(kind: TypeHierarchyKind): string { return kind === 'supertypes' ? 'Super types' : 'Sub types' } ================================================ FILE: src/handler/types.ts ================================================ import type { CodeAction, CodeActionKind, Position, Range, SymbolKind } from 'vscode-languageserver-types' import type { ProviderName } from '../languages' import type Document from '../model/document' import type { TextDocumentMatch } from '../types' import type { CancellationToken, Disposable } from '../util/protocol' export interface CurrentState { doc: Document winid: number position: Position // :h mode() mode: string } export interface HandlerDelegate { uri: string | undefined checkProvider: (id: ProviderName, document: TextDocumentMatch) => void withRequestToken: (name: string, fn: (token: CancellationToken) => Thenable, checkEmpty?: boolean) => Promise getCurrentState: () => Promise addDisposable: (disposable: Disposable) => void getIcon(kind: SymbolKind): { text: string, hlGroup: string } getCodeActions(doc: Document, range?: Range, only?: CodeActionKind[]): Promise applyCodeAction(action: CodeAction): Promise } ================================================ FILE: src/handler/util.ts ================================================ import { MarkupContent } from 'vscode-languageserver-types' import { Documentation } from '../types' import { isMarkdown } from '../util/is' export function toDocumentation(doc: string | MarkupContent): Documentation { return { content: typeof doc === 'string' ? doc : doc.value, filetype: isMarkdown(doc) ? 'markdown' : 'txt' } } ================================================ FILE: src/handler/workspace.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { v4 as uuid } from 'uuid' import { writeHeapSnapshot } from 'v8' import { Location } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import type { WorkspaceConfiguration } from '../configuration/types' import { callAsync } from '../core/funcs' import { PatternType } from '../core/workspaceFolder' import extensions from '../extension' import languages, { ProviderName } from '../languages' import { getLoggerFile } from '../logger' import Highlighter from '../model/highlighter' import snippetManager from '../snippets/manager' import { defaultValue } from '../util' import { CONFIG_FILE_NAME, isVim } from '../util/constants' import { directoryNotExists } from '../util/errors' import { isDirectory } from '../util/fs' import * as Is from '../util/is' import { fs, os, path } from '../util/node' import { toText } from '../util/string' import window from '../window' import workspace from '../workspace' declare const REVISION interface RootPatterns { buffer: ReadonlyArray server: ReadonlyArray global: ReadonlyArray } export default class WorkspaceHandler { constructor( private nvim: Neovim ) { // exported by window. Object.defineProperty(window, 'openLocalConfig', { get: () => this.openLocalConfig.bind(this) }) extensions.onDidUnloadExtension(name => { workspace.autocmds.removeExtensionAutocmds(name) }) commands.register({ id: 'workspace.openLocation', execute: async (winid: number, loc: Location, openCommand?: string) => { await nvim.call('win_gotoid', [winid]) await workspace.jumpTo(loc.uri, loc.range.start, openCommand) } }, true) commands.register({ id: 'workspace.openLocalConfig', execute: async () => { await this.openLocalConfig() } }, false, 'Open config file of current workspace folder') commands.register({ id: 'workspace.undo', execute: async () => { await workspace.files.undoWorkspaceEdit() } }, false, 'Undo previous this.workspace edit') commands.register({ id: 'workspace.redo', execute: async () => { await workspace.files.redoWorkspaceEdit() } }, false, 'Redo previous this.workspace edit') commands.register({ id: 'workspace.inspectEdit', execute: async () => { await workspace.files.inspectEdit() } }, false, 'Inspect previous this.workspace edit in new tab') commands.register({ id: 'workspace.renameCurrentFile', execute: async () => { await this.renameCurrent() } }, false, 'change current filename to a new name and reload it.') commands.register({ id: 'document.checkBuffer', execute: async () => { await this.bufferCheck() } }, false, 'Check providers for current buffer.') commands.register({ id: 'document.echoFiletype', execute: async () => { let bufnr = await nvim.call('bufnr', '%') as number let doc = workspace.getAttachedDocument(bufnr) await window.echoLines([doc.filetype]) } }, false, 'echo the mapped filetype of the current buffer') commands.register({ id: 'workspace.workspaceFolders', execute: async () => { let folders = workspace.workspaceFolders let lines = folders.map(folder => URI.parse(folder.uri).fsPath) await window.echoLines(lines) } }, false, 'show opened workspaceFolders.') commands.register({ id: 'workspace.writeHeapSnapshot', execute: async () => { let filepath = path.join(os.homedir(), `${uuid()}-${process.pid}.heapsnapshot`) writeHeapSnapshot(filepath) void window.showInformationMessage(`Create heapdump at: ${filepath}`) return filepath } }, false, 'Generates a snapshot of the current V8 heap and writes it to a JSON file.') commands.register({ id: 'workspace.showOutput', execute: async (name?: string, cmd?: string) => { if (!name) name = await window.showQuickPick(workspace.channelNames, { title: 'Choose output name' }) as string window.showOutputChannel(toText(name), cmd) } }, false, 'open output buffer to show output from languageservers or extensions.') commands.register({ id: 'workspace.clearWatchman', execute: async () => { let res = await window.runTerminalCommand('watchman watch-del-all') if (res.success) void window.showInformationMessage('Cleared watchman watching directories.') return res.success } }, false, 'run watch-del-all for watchman to free up memory.') } public async openLog(): Promise { let file = getLoggerFile() await workspace.jumpTo(URI.file(file).toString()) } /** * Open local config file */ public async openLocalConfig(): Promise { let fsPath = await this.nvim.call('coc#util#get_fullpath', []) as string let filetype = await this.nvim.eval('&filetype') as string if (!fsPath || !path.isAbsolute(fsPath)) { void window.showWarningMessage(`Current buffer doesn't have valid file path.`) return } let folder = workspace.getWorkspaceFolder(URI.file(fsPath).toString()) if (!folder) { let c = workspace.initialConfiguration.get('workspace') let patterns = defaultValue(c.rootPatterns, []) let ignored = defaultValue(c.ignoredFiletypes, []) let msg: string if (ignored.includes(filetype)) msg = `Filetype '${filetype}' is ignored for workspace folder resolve.` if (!msg) msg = `Can't resolve workspace folder for file '${fsPath}, consider create one of ${patterns.join(', ')} in your project root.'.` void window.showWarningMessage(msg) return } let root = URI.parse(folder.uri).fsPath let dir = path.join(root, '.vim') if (!fs.existsSync(dir)) { let res = await window.showPrompt(`Would you like to create folder'${root}/.vim'?`) if (!res) return fs.mkdirSync(dir) } await workspace.jumpTo(URI.file(path.join(dir, CONFIG_FILE_NAME))) } public async renameCurrent(): Promise { let { nvim } = this let oldPath = await nvim.call('coc#util#get_fullpath', []) as string let newPath = await callAsync(nvim, 'input', ['New path: ', oldPath, 'file']) as string newPath = newPath.trim() if (newPath === oldPath || !newPath) return if (oldPath.toLowerCase() != newPath.toLowerCase() && fs.existsSync(newPath)) { let overwrite = await window.showPrompt(`${newPath} exists, overwrite?`) if (!overwrite) return } await workspace.renameFile(oldPath, newPath, { overwrite: true }) } public addWorkspaceFolder(folder: string): void { if (!Is.string(folder)) throw TypeError(`folder should be string`) folder = workspace.expand(folder) if (!isDirectory(folder)) throw directoryNotExists(folder) workspace.workspaceFolderControl.addWorkspaceFolder(folder, true) } public removeWorkspaceFolder(folder: string): void { if (!Is.string(folder)) throw TypeError(`folder should be string`) folder = workspace.expand(folder) if (!isDirectory(folder)) throw directoryNotExists(folder) workspace.workspaceFolderControl.removeWorkspaceFolder(folder) } public async bufferCheck(): Promise { let doc = await workspace.document if (!doc.attached) { await window.showDialog({ title: 'Buffer check result', content: `Document not attached, ${doc.notAttachReason}`, highlight: 'WarningMsg' }) return } let hi = new Highlighter() hi.addLine('Provider state', 'Title') hi.addLine('') for (let name of Object.values(ProviderName)) { if (name === ProviderName.OnTypeEdit) continue let exists = languages.hasProvider(name, doc.textDocument) hi.addTexts([ { text: '-', hlGroup: 'Comment' }, { text: ' ' }, exists ? { text: '✓', hlGroup: 'CocListFgGreen' } : { text: '✗', hlGroup: 'CocListFgRed' }, { text: ' ' }, { text: name, hlGroup: exists ? 'Normal' : 'CocFadeOut' } ]) } await window.showDialog({ title: 'Buffer check result', content: hi.content, highlights: hi.highlights }) } public async doAutocmd(id: number, args: any[]): Promise { let timeout = workspace.getConfiguration('editor', null).get('timeout', 1000) await workspace.autocmds.doAutocmd(id, args, timeout) } public async getConfiguration(key: string): Promise { let document = await workspace.document return workspace.getConfiguration(key, document) } public getRootPatterns(bufnr: number): RootPatterns | null { let doc = workspace.getDocument(bufnr) if (!doc) return null return { buffer: workspace.workspaceFolderControl.getRootPatterns(doc, PatternType.Buffer), server: workspace.workspaceFolderControl.getRootPatterns(doc, PatternType.LanguageServer), global: workspace.workspaceFolderControl.getRootPatterns(doc, PatternType.Global) } } public async ensureDocument(bufnr?: number): Promise { let doc = bufnr ? workspace.getDocument(bufnr) : await workspace.document return doc && doc.attached } public async doKeymap(key: string, defaultReturn = ''): Promise { return await workspace.keymaps.doKeymap(key, defaultReturn) } public async snippetCheck(checkExpand: boolean, checkJump: boolean): Promise { if (checkJump) { let jumpable = snippetManager.jumpable() if (jumpable) return true } if (checkExpand) { let expandable = await Promise.resolve(extensions.manager.call('coc-snippets', 'expandable', [])) if (expandable) return true } return false } public async showInfo(): Promise { let lines: string[] = [] let version = workspace.version + (typeof REVISION === 'string' ? '-' + REVISION : '') lines.push('## versions') lines.push('') let out = await this.nvim.call('execute', ['version']) as string let first = out.trim().split(/\r?\n/, 2)[0].replace(/\(.*\)/, '').trim() lines.push('vim version: ' + first + `${isVim ? ' ' + workspace.env.version : ''}`) lines.push('node version: ' + process.version) lines.push('coc.nvim version: ' + version) lines.push('coc.nvim directory: ' + path.dirname(__dirname)) lines.push('term: ' + defaultValue(process.env.TERM_PROGRAM, process.env.TERM)) lines.push('platform: ' + process.platform) lines.push('') lines.push('## Log of coc.nvim') lines.push('') let file = getLoggerFile() const stripAnsi = require('strip-ansi') if (fs.existsSync(file)) { let content = fs.readFileSync(file, { encoding: 'utf8' }) lines.push(...content.split(/\r?\n/).map(line => stripAnsi(line))) } await this.nvim.command('vnew +setl\\ buftype=nofile\\ bufhidden=wipe\\ nobuflisted') let buf = await this.nvim.buffer await buf.setLines(lines, { start: 0, end: -1, strictIndexing: false }) } } ================================================ FILE: src/index.ts ================================================ 'use strict' import { AnnotatedTextEdit, ApplyKind, ChangeAnnotation, ChangeAnnotationIdentifier, CodeAction, CodeActionContext, CodeActionKind, CodeActionTriggerKind, CodeDescription, CodeLens, Color, ColorInformation, ColorPresentation, Command, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionItemTag, CompletionList, CreateFile, DeleteFile, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, DocumentHighlight, DocumentHighlightKind, DocumentLink, DocumentSymbol, DocumentUri, FoldingRange, FoldingRangeKind, FormattingOptions, Hover, InlayHint, InlayHintKind, InlayHintLabelPart, InlineCompletionList, InlineCompletionTriggerKind, InlineValueContext, InlineValueEvaluatableExpression, InlineValueText, InlineValueVariableLookup, InsertReplaceEdit, InsertTextFormat, InsertTextMode, integer, Location, LocationLink, MarkedString, MarkupContent, MarkupKind, OptionalVersionedTextDocumentIdentifier, ParameterInformation, Position, Range, RenameFile, SelectedCompletionInfo, SelectionRange, SemanticTokenModifiers, SemanticTokens, SemanticTokenTypes, SignatureInformation, SnippetTextEdit, StringValue, SymbolInformation, SymbolKind, SymbolTag, TextDocumentEdit, TextDocumentIdentifier, TextDocumentItem, TextEdit, uinteger, VersionedTextDocumentIdentifier, WorkspaceChange, WorkspaceEdit, WorkspaceFolder, WorkspaceSymbol } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from './commands' import sources from './completion/sources' import diagnosticManager from './diagnostic/manager' import events from './events' import extensions from './extension' import languages, { ProviderName } from './languages' import BasicList from './list/basic' import listManager from './list/manager' import download from './model/download' import fetch from './model/fetch' import FloatFactory from './model/floatFactory' import Highlighter from './model/highlighter' import Mru from './model/mru' import RelativePattern from './model/relativePattern' import services, { ServiceStat } from './services' import snippetManager from './snippets/manager' import { SnippetString } from './snippets/string' import { ansiparse } from './util/ansiparse' import { CancellationError } from './util/errors' import { Mutex } from './util/mutex' import { CancellationToken, CancellationTokenSource, CompletionTriggerKind, Disposable, DocumentDiagnosticReportKind, Emitter, ErrorCodes, Event, FileChangeType, InlineCompletionContext, InlineCompletionItem, MonikerKind, NotificationType, NotificationType0, ProgressType, ProtocolNotificationType, ProtocolNotificationType0, ProtocolRequestType, ProtocolRequestType0, RequestType, RequestType0, ResponseError, SignatureHelpTriggerKind, Trace, UniquenessLevel, } from './util/protocol' import window from './window' import workspace from './workspace' import { ClientState, CloseAction, DiagnosticPullMode, ErrorAction, LanguageClient, MessageTransports, NullLogger, RevealOutputChannelOn, SettingMonitor, State, TransportKind } from './language-client' import { SourceType } from './completion/types' import { ConfigurationUpdateTarget } from './configuration/types' import { PatternType } from './core/workspaceFolder' import LineBuilder from './model/line' import { SemanticTokensBuilder } from './model/semanticTokensBuilder' import { TreeItem, TreeItemCollapsibleState } from './tree/index' import { concurrent, disposeAll, wait } from './util' import { FileType, watchFile } from './util/fs' import { executable, isRunning, runCommand, terminate } from './util/processes' module.exports = { get nvim() { return workspace.nvim }, Uri: URI, LineBuilder, NullLogger, SettingMonitor, LanguageClient, CancellationTokenSource, ProgressType, RequestType, RequestType0, NotificationType, NotificationType0, ProtocolRequestType, ProtocolRequestType0, ProtocolNotificationType, ProtocolNotificationType0, Highlighter, Mru, Emitter, SnippetString, BasicList, Mutex, TreeItem, SemanticTokensBuilder, FloatFactory, RelativePattern, CancellationError, WorkspaceChange, ResponseError, StringValue, SnippetTextEdit, Trace, DocumentUri, WorkspaceFolder, SelectedCompletionInfo, InlineCompletionContext, InlineCompletionItem, InlineCompletionList, InlineCompletionTriggerKind, InlineValueText, InlineValueVariableLookup, InlineValueEvaluatableExpression, InlineValueContext, InlayHintKind, InlayHintLabelPart, InlayHint, DiagnosticRelatedInformation, SemanticTokens, SemanticTokenTypes, SemanticTokenModifiers, AnnotatedTextEdit, ChangeAnnotation, SymbolTag, Command, Color, CodeDescription, ColorInformation, ColorPresentation, TextDocumentEdit, TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentItem, DocumentHighlight, SelectionRange, DocumentLink, CodeLens, FormattingOptions, CodeAction, CodeActionContext, DocumentSymbol, WorkspaceSymbol, CreateFile, RenameFile, WorkspaceEdit, InsertReplaceEdit, InsertTextMode, CompletionItem, CompletionList, Hover, ParameterInformation, SignatureInformation, SymbolInformation, MarkupContent, ErrorCodes, CompletionItemTag, integer, uinteger, FoldingRangeKind, FoldingRange, ChangeAnnotationIdentifier, DeleteFile, OptionalVersionedTextDocumentIdentifier, CompletionItemLabelDetails, MarkedString, ProviderName, DocumentDiagnosticReportKind, UniquenessLevel, MonikerKind, PatternType, SourceType, ConfigurationTarget: ConfigurationUpdateTarget, ServiceStat, FileType, State, ClientState, CloseAction, ErrorAction, TransportKind, MessageTransports, RevealOutputChannelOn, MarkupKind, DiagnosticTag, DocumentHighlightKind, SymbolKind, SignatureHelpTriggerKind, FileChangeType, CodeActionKind, Diagnostic, DiagnosticSeverity, CompletionItemKind, InsertTextFormat, Location, LocationLink, CancellationToken, Position, Range, TextEdit, Disposable, Event, workspace, window, CodeActionTriggerKind, CompletionTriggerKind, snippetManager, events, services, commands, sources, languages, diagnosticManager, extensions, listManager, TreeItemCollapsibleState, DiagnosticPullMode, ApplyKind, terminate, fetch, download, ansiparse, disposeAll, concurrent, watchFile, wait, runCommand, isRunning, executable, } ================================================ FILE: src/language-client/LICENSE.txt ================================================ MIT License Copyright (c) 2015 - present Microsoft Corporation All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: src/language-client/callHierarchy.ts ================================================ 'use strict' import type { CallHierarchyClientCapabilities, CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOptions, CallHierarchyOutgoingCall, CallHierarchyRegistrationOptions, CancellationToken, ClientCapabilities, Disposable, DocumentSelector, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { CallHierarchyProvider, ProviderResult } from '../provider' import { CallHierarchyIncomingCallsRequest, CallHierarchyOutgoingCallsRequest, CallHierarchyPrepareRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface PrepareCallHierarchySignature { (this: void, document: TextDocument, position: Position, token: CancellationToken): ProviderResult } export interface CallHierarchyIncomingCallsSignature { (this: void, item: CallHierarchyItem, token: CancellationToken): ProviderResult } export interface CallHierarchyOutgoingCallsSignature { (this: void, item: CallHierarchyItem, token: CancellationToken): ProviderResult } /** * Call hierarchy middleware * @since 3.16.0 */ export interface CallHierarchyMiddleware { prepareCallHierarchy?: (this: void, document: TextDocument, positions: Position, token: CancellationToken, next: PrepareCallHierarchySignature) => ProviderResult provideCallHierarchyIncomingCalls?: (this: void, item: CallHierarchyItem, token: CancellationToken, next: CallHierarchyIncomingCallsSignature) => ProviderResult provideCallHierarchyOutgoingCalls?: (this: void, item: CallHierarchyItem, token: CancellationToken, next: CallHierarchyOutgoingCallsSignature) => ProviderResult } export class CallHierarchyFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, CallHierarchyPrepareRequest.type) } public fillClientCapabilities(cap: ClientCapabilities): void { const capabilities: ClientCapabilities & CallHierarchyClientCapabilities = cap as ClientCapabilities & CallHierarchyClientCapabilities const capability = ensure(ensure(capabilities, 'textDocument')!, 'callHierarchy')! capability.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const [id, options] = this.getRegistration(documentSelector, capabilities.callHierarchyProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: CallHierarchyRegistrationOptions): [Disposable, CallHierarchyProvider] { const provider: CallHierarchyProvider = { prepareCallHierarchy: (document: TextDocument, position: Position, token: CancellationToken) => { const client = this._client const prepareCallHierarchy: PrepareCallHierarchySignature = (document, position, token) => { const params = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position) return this.sendRequest(CallHierarchyPrepareRequest.type, params, token) } const middleware = client.middleware return middleware.prepareCallHierarchy ? middleware.prepareCallHierarchy(document, position, token, prepareCallHierarchy) : prepareCallHierarchy(document, position, token) }, provideCallHierarchyIncomingCalls: (item: CallHierarchyItem, token: CancellationToken) => { const client = this._client const provideCallHierarchyIncomingCalls: CallHierarchyIncomingCallsSignature = (item, token) => { return this.sendRequest(CallHierarchyIncomingCallsRequest.type, { item }, token) } const middleware = client.middleware return middleware.provideCallHierarchyIncomingCalls ? middleware.provideCallHierarchyIncomingCalls(item, token, provideCallHierarchyIncomingCalls) : provideCallHierarchyIncomingCalls(item, token) }, provideCallHierarchyOutgoingCalls: (item: CallHierarchyItem, token: CancellationToken) => { const client = this._client const provideCallHierarchyOutgoingCalls: CallHierarchyOutgoingCallsSignature = (item, token) => { return this.sendRequest(CallHierarchyOutgoingCallsRequest.type, { item }, token) } const middleware = client.middleware return middleware.provideCallHierarchyOutgoingCalls ? middleware.provideCallHierarchyOutgoingCalls(item, token, provideCallHierarchyOutgoingCalls) : provideCallHierarchyOutgoingCalls(item, token) } } this._client.attachExtensionName(provider) return [languages.registerCallHierarchyProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/client.ts ================================================ 'use strict' import { ApplyWorkspaceEditParams, ApplyWorkspaceEditResult, CallHierarchyPrepareRequest, CancellationStrategy, CancellationToken, ClientCapabilities, CodeActionRequest, CodeLensRequest, CompletionRequest, ConfigurationRequest, ConnectionStrategy, DeclarationRequest, DefinitionRequest, DidChangeConfigurationNotification, DidChangeConfigurationRegistrationOptions, DidChangeTextDocumentNotification, DidChangeWatchedFilesNotification, DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersNotification, DidCloseTextDocumentNotification, DidCloseTextDocumentParams, DidCreateFilesNotification, DidDeleteFilesNotification, DidOpenTextDocumentNotification, DidRenameFilesNotification, DidSaveTextDocumentNotification, Disposable, DocumentColorRequest, DocumentDiagnosticRequest, DocumentFormattingRequest, DocumentHighlightRequest, DocumentLinkRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest, DocumentSelector, DocumentSymbolRequest, ExecuteCommandRegistrationOptions, ExecuteCommandRequest, FileEvent, FileOperationRegistrationOptions, FoldingRangeRequest, GenericNotificationHandler, GenericRequestHandler, HandlerResult, HoverRequest, ImplementationRequest, InitializeParams, InitializeResult, InlineCompletionRequest, InlineValueRequest, LinkedEditingRangeRequest, Message, MessageActionItem, MessageSignature, NotificationHandler, NotificationHandler0, NotificationType, NotificationType0, ProgressToken, ProgressType, ProtocolNotificationType, ProtocolNotificationType0, ProtocolRequestType, ProtocolRequestType0, PublishDiagnosticsParams, ReferencesRequest, RegistrationParams, RenameRequest, RequestHandler, RequestHandler0, RequestParam, RequestType, RequestType0, SelectionRangeRequest, SemanticTokensRegistrationType, ServerCapabilities, ShowDocumentParams, ShowDocumentResult, ShowMessageRequestParams, SignatureHelpRequest, TextDocumentContentRequest, TextDocumentRegistrationOptions, TextDocumentSyncOptions, TextEdit, TraceOptions, Tracer, TypeDefinitionRequest, TypeHierarchyPrepareRequest, UnregistrationParams, WillCreateFilesRequest, WillDeleteFilesRequest, WillRenameFilesRequest, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest, WorkDoneProgressBegin, WorkDoneProgressCreateRequest, WorkDoneProgressEnd, WorkDoneProgressReport, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import { Diagnostic, DiagnosticTag, MarkupKind } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { FileCreateEvent, FileDeleteEvent, FileRenameEvent, FileWillCreateEvent, FileWillDeleteEvent, FileWillRenameEvent, TextDocumentWillSaveEvent } from '../core/files' import DiagnosticCollection from '../diagnostic/collection' import languages from '../languages' import { createLogger } from '../logger' import type { MessageItem } from '../model/notification' import { CallHierarchyProvider, CodeActionProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentLinkProvider, DocumentRangeFormattingEditProvider, DocumentSymbolProvider, HoverProvider, ImplementationProvider, InlineCompletionItemProvider, LinkedEditingRangeProvider, OnTypeFormattingEditProvider, ProviderResult, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, TypeHierarchyProvider, WorkspaceSymbolProvider } from '../provider' import { OutputChannel, Thenable } from '../types' import { defaultValue, disposeAll, getConditionValue } from '../util' import { isFalsyOrEmpty, toArray } from '../util/array' import { CancellationError, onUnexpectedError } from '../util/errors' import { parseExtensionName } from '../util/extensionRegistry' import { sameFile } from '../util/fs' import * as Is from '../util/is' import { os } from '../util/node' import { comparePosition } from '../util/position' import { ApplyWorkspaceEditRequest, createProtocolConnection, Emitter, ErrorCodes, Event, ExitNotification, FailureHandlingKind, InitializedNotification, InitializeRequest, InlayHintRequest, LogMessageNotification, LSPErrorCodes, MessageReader, MessageType, MessageWriter, PositionEncodingKind, PublishDiagnosticsNotification, RegistrationRequest, ResourceOperationKind, ResponseError, SemanticTokensDeltaRequest, SemanticTokensRangeRequest, SemanticTokensRequest, ShowDocumentRequest, ShowMessageNotification, ShowMessageRequest, ShutdownRequest, TextDocumentSyncKind, Trace, TraceFormat, UnregistrationRequest, WorkDoneProgress } from '../util/protocol' import { toText } from '../util/string' import window from '../window' import workspace from '../workspace' import { CallHierarchyFeature, CallHierarchyMiddleware } from './callHierarchy' import { CodeActionFeature, CodeActionMiddleware } from './codeAction' import { CodeLensFeature, CodeLensMiddleware, CodeLensProviderShape } from './codeLens' import { ColorProviderFeature, ColorProviderMiddleware } from './colorProvider' import { $CompletionOptions, CompletionItemFeature, CompletionMiddleware } from './completion' import { $ConfigurationOptions, ConfigurationMiddleware, DidChangeConfigurationMiddleware, PullConfigurationFeature, SyncConfigurationFeature } from './configuration' import { DeclarationFeature, DeclarationMiddleware } from './declaration' import { DefinitionFeature, DefinitionMiddleware } from './definition' import { $DiagnosticPullOptions, DiagnosticFeature, DiagnosticFeatureShape, DiagnosticProviderMiddleware, DiagnosticProviderShape, DiagnosticPullMode } from './diagnostic' import { DocumentHighlightFeature, DocumentHighlightMiddleware } from './documentHighlight' import { DocumentLinkFeature, DocumentLinkMiddleware } from './documentLink' import { DocumentSymbolFeature, DocumentSymbolMiddleware } from './documentSymbol' import { ExecuteCommandFeature, ExecuteCommandMiddleware } from './executeCommand' import { Connection, DynamicFeature, ensure, FeatureClient, LSPCancellationError, RegistrationData, StaticFeature, TextDocumentProviderFeature, TextDocumentSendFeature } from './features' import { DidCreateFilesFeature, DidDeleteFilesFeature, DidRenameFilesFeature, FileOperationsMiddleware, WillCreateFilesFeature, WillDeleteFilesFeature, WillRenameFilesFeature } from './fileOperations' import { DidChangeWatchedFileSignature, FileSystemWatcherFeature } from './fileSystemWatcher' import { FoldingRangeFeature, FoldingRangeProviderMiddleware, FoldingRangeProviderShape } from './foldingRange' import { $FormattingOptions, DocumentFormattingFeature, DocumentOnTypeFormattingFeature, DocumentRangeFormattingFeature, FormattingMiddleware } from './formatting' import { HoverFeature, HoverMiddleware } from './hover' import { ImplementationFeature, ImplementationMiddleware } from './implementation' import { InlayHintsFeature, InlayHintsMiddleware, InlayHintsProviderShape } from './inlayHint' import { InlineCompletionItemFeature, InlineCompletionMiddleware } from './inlineCompletion' import { InlineValueFeature, InlineValueMiddleware, InlineValueProviderShape } from './inlineValue' import { LinkedEditingFeature, LinkedEditingRangeMiddleware } from './linkedEditingRange' import { ProgressFeature } from './progress' import { ProgressPart } from './progressPart' import { ReferencesFeature, ReferencesMiddleware } from './reference' import { RenameFeature, RenameMiddleware } from './rename' import { SelectionRangeFeature, SelectionRangeProviderMiddleware } from './selectionRange' import { SemanticTokensFeature, SemanticTokensMiddleware, SemanticTokensProviderShape } from './semanticTokens' import { SignatureHelpFeature, SignatureHelpMiddleware } from './signatureHelp' import { TextDocumentContentFeature, TextDocumentContentMiddleware, TextDocumentContentProviderShape } from './textDocumentContent' import { DidChangeTextDocumentFeature, DidChangeTextDocumentFeatureShape, DidCloseTextDocumentFeature, DidCloseTextDocumentFeatureShape, DidOpenTextDocumentFeature, DidOpenTextDocumentFeatureShape, DidSaveTextDocumentFeature, DidSaveTextDocumentFeatureShape, ResolvedTextDocumentSyncCapabilities, TextDocumentSynchronizationMiddleware, WillSaveFeature, WillSaveWaitUntilFeature } from './textSynchronization' import { TypeDefinitionFeature, TypeDefinitionMiddleware } from './typeDefinition' import { TypeHierarchyFeature, TypeHierarchyMiddleware } from './typeHierarchy' import { currentTimeStamp, data2String, fixNotificationType, fixRequestType, getLocale, getTracePrefix, toMethod } from './utils' import { Delayer } from './utils/async' import * as c2p from './utils/codeConverter' import { CloseAction, CloseHandlerResult, DefaultErrorHandler, ErrorAction, ErrorHandler, ErrorHandlerResult, InitializationFailedHandler, toCloseHandlerResult } from './utils/errorHandler' import { ConsoleLogger, NullLogger } from './utils/logger' import * as UUID from './utils/uuid' import { $WorkspaceOptions, WorkspaceFolderMiddleware, WorkspaceFoldersFeature } from './workspaceFolders' import { WorkspaceProviderFeature, WorkspaceSymbolFeature, WorkspaceSymbolMiddleware } from './workspaceSymbol' const logger = createLogger('language-client-client') export { CloseAction, DiagnosticPullMode, ErrorAction, NullLogger } interface ConnectionErrorHandler { (error: Error, message: Message | undefined, count: number | undefined): void } interface ConnectionCloseHandler { (): void } interface ConnectionOptions { cancellationStrategy?: CancellationStrategy connectionStrategy?: ConnectionStrategy maxRestartCount?: number } const redOpen = '\x1B[31m' const redClose = '\x1B[39m' function createConnection(input: MessageReader, output: MessageWriter, errorHandler: ConnectionErrorHandler, closeHandler: ConnectionCloseHandler, options?: ConnectionOptions): Connection { let logger = new ConsoleLogger() let connection = createProtocolConnection(input, output, logger, options) connection.onError(data => { errorHandler(data[0], data[1], data[2]) }) connection.onClose(closeHandler) let result: Connection = { id: '', listen: (): void => connection.listen(), hasPendingResponse: connection.hasPendingResponse, sendRequest: connection.sendRequest, onRequest: connection.onRequest, sendNotification: connection.sendNotification, onNotification: connection.onNotification, onProgress: connection.onProgress, sendProgress: connection.sendProgress, trace: (value: Trace, tracer: Tracer, traceOptions: TraceOptions): Promise => { return connection.trace(value, tracer, traceOptions) }, initialize: (params: InitializeParams) => { return connection.sendRequest(InitializeRequest.type, params) }, shutdown: () => { return connection.sendRequest(ShutdownRequest.type, undefined) }, exit: () => { return connection.sendNotification(ExitNotification.type) }, end: () => connection.end(), dispose: () => connection.dispose() } return result } export enum RevealOutputChannelOn { Debug = 0, Info = 1, Warn = 2, Error = 3, Never = 4 } export interface HandleWorkDoneProgressSignature { (this: void, token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd): void } export interface HandleDiagnosticsSignature { (this: void, uri: string, diagnostics: Diagnostic[]): void } interface _WorkspaceMiddleware { didChangeWatchedFile?: (this: void, event: FileEvent, next: DidChangeWatchedFileSignature) => Promise handleApplyEdit?: (this: void, params: ApplyWorkspaceEditParams, next: ApplyWorkspaceEditRequest.HandlerSignature) => HandlerResult } export type WorkspaceMiddleware = _WorkspaceMiddleware & ConfigurationMiddleware & DidChangeConfigurationMiddleware & WorkspaceFolderMiddleware & FileOperationsMiddleware export interface _WindowMiddleware { showDocument?: ShowDocumentRequest.MiddlewareSignature } export type WindowMiddleware = _WindowMiddleware /** * The Middleware lets extensions intercept the request and notifications send and received * from the server */ export interface _Middleware { handleDiagnostics?: (this: void, uri: string, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => void handleWorkDoneProgress?: (this: void, token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd, next: HandleWorkDoneProgressSignature) => void handleRegisterCapability?: (this: void, params: RegistrationParams, next: RegistrationRequest.HandlerSignature) => Promise handleUnregisterCapability?: (this: void, params: UnregistrationParams, next: UnregistrationRequest.HandlerSignature) => Promise workspace?: WorkspaceMiddleware window?: WindowMiddleware } // A general middleware is applied to both requests and notifications interface GeneralMiddleware { sendRequest?( this: void, type: string | MessageSignature, param: P | undefined, token: CancellationToken | undefined, next: (type: string | MessageSignature, param?: P, token?: CancellationToken) => Promise, ): Promise sendNotification?( this: void, type: string | MessageSignature, next: (type: string | MessageSignature, params?: R) => Promise, params: R ): Promise } export type Middleware = _Middleware & TextDocumentSynchronizationMiddleware & SignatureHelpMiddleware & ReferencesMiddleware & DefinitionMiddleware & DocumentHighlightMiddleware & DocumentSymbolMiddleware & DocumentLinkMiddleware & CodeActionMiddleware & FormattingMiddleware & RenameMiddleware & CodeLensMiddleware & HoverMiddleware & CompletionMiddleware & ExecuteCommandMiddleware & TypeDefinitionMiddleware & ImplementationMiddleware & ColorProviderMiddleware & DeclarationMiddleware & FoldingRangeProviderMiddleware & CallHierarchyMiddleware & SemanticTokensMiddleware & InlayHintsMiddleware & InlineCompletionMiddleware & InlineValueMiddleware & TypeHierarchyMiddleware & WorkspaceSymbolMiddleware & DiagnosticProviderMiddleware & LinkedEditingRangeMiddleware & SelectionRangeProviderMiddleware & GeneralMiddleware & TextDocumentContentMiddleware export type LanguageClientOptions = { rootPatterns?: string[] requireRootPattern?: boolean documentSelector?: DocumentSelector disableMarkdown?: boolean disableDiagnostics?: boolean diagnosticCollectionName?: string disableDynamicRegister?: boolean disabledFeatures?: string[] outputChannelName?: string traceOutputChannel?: OutputChannel outputChannel?: OutputChannel revealOutputChannelOn?: RevealOutputChannelOn /** * The encoding use to read stdout and stderr. Defaults * to 'utf8' if omitted. */ stdioEncoding?: string initializationOptions?: any | (() => any) initializationFailedHandler?: InitializationFailedHandler progressOnInitialization?: boolean errorHandler?: ErrorHandler middleware?: Middleware uriConverter?: { code2Protocol: c2p.URIConverter } connectionOptions?: ConnectionOptions markdown?: { isTrusted?: boolean supportHtml?: boolean } textSynchronization?: { /** * Delays sending the open notification until one of the following * conditions becomes `true`: * - document is visible in the editor. * - any of the other notifications or requests is sent to the server, except * a closed notification for the pending document. */ delayOpenNotifications?: boolean } } & $ConfigurationOptions & $CompletionOptions & $FormattingOptions & $DiagnosticPullOptions & $WorkspaceOptions type ResolvedClientOptions = { disabledFeatures: string[] disableMarkdown: boolean disableDynamicRegister: boolean rootPatterns?: string[] requireRootPattern?: boolean documentSelector: DocumentSelector diagnosticCollectionName?: string outputChannelName: string revealOutputChannelOn: RevealOutputChannelOn stdioEncoding: string initializationOptions?: any | (() => any) initializationFailedHandler?: InitializationFailedHandler progressOnInitialization: boolean errorHandler: ErrorHandler middleware: Middleware uriConverter?: { code2Protocol: c2p.URIConverter } connectionOptions?: ConnectionOptions markdown: { isTrusted: boolean supportHtml?: boolean } textSynchronization: { delayOpenNotifications?: boolean } } & $ConfigurationOptions & Required<$CompletionOptions> & Required<$FormattingOptions> & Required<$DiagnosticPullOptions> & Required<$WorkspaceOptions> export enum State { Stopped = 1, Running = 2, Starting = 3, StartFailed = 4, } export interface StateChangeEvent { oldState: State newState: State } export enum ClientState { Initial, Starting, StartFailed, Running, Stopping, Stopped } export interface MessageTransports { reader: MessageReader writer: MessageWriter detached?: boolean } // eslint-disable-next-line no-redeclare export namespace MessageTransports { export function is(value: any): value is MessageTransports { let candidate: MessageTransports = value return ( candidate && MessageReader.is(value.reader) && MessageWriter.is(value.writer) ) } } export enum ShutdownMode { Restart = 'restart', Stop = 'stop' } const delayTime = getConditionValue(250, 10) export abstract class BaseLanguageClient implements FeatureClient { private _rootPath: string | false private _consoleDebug = false private __extensionName: string private _id: string private _name: string private _clientOptions: ResolvedClientOptions protected _state: ClientState private _onStart: Promise | undefined private _onStop: Promise | undefined private _connection: Connection | undefined private _initializeResult: InitializeResult | undefined private _outputChannel: OutputChannel | undefined private _traceOutputChannel: OutputChannel | undefined private _capabilities: ServerCapabilities & ResolvedTextDocumentSyncCapabilities private _disposed: 'disposing' | 'disposed' | undefined private readonly _ignoredRegistrations: Set private readonly _listeners: Disposable[] private readonly _notificationHandlers: Map private readonly _notificationDisposables: Map private readonly _pendingNotificationHandlers: Map private readonly _requestHandlers: Map> private readonly _requestDisposables: Map private readonly _pendingRequestHandlers: Map> private readonly _progressHandlers: Map; handler: NotificationHandler }> private readonly _pendingProgressHandlers: Map; handler: NotificationHandler }> private readonly _progressDisposables: Map private _fileEvents: FileEvent[] private _fileEventDelayer: Delayer private _diagnostics: DiagnosticCollection | undefined private _syncedDocuments: Map private _traceFormat: TraceFormat private _trace: Trace private _tracer: Tracer private _stateChangeEmitter: Emitter private readonly _c2p: c2p.Converter private _didOpenTextDocumentFeature: DidOpenTextDocumentFeature | undefined public constructor( id: string, name: string, clientOptions: LanguageClientOptions ) { this._id = id this._name = name if (clientOptions.outputChannel) { this._outputChannel = clientOptions.outputChannel } else { this._outputChannel = undefined } this._traceOutputChannel = clientOptions.traceOutputChannel this._clientOptions = this.resolveClientOptions(clientOptions) this.$state = ClientState.Initial this._connection = undefined this._initializeResult = undefined this._listeners = [] this._diagnostics = undefined this._notificationHandlers = new Map() this._pendingNotificationHandlers = new Map() this._notificationDisposables = new Map() this._requestHandlers = new Map() this._pendingRequestHandlers = new Map() this._requestDisposables = new Map() this._progressHandlers = new Map() this._pendingProgressHandlers = new Map() this._progressDisposables = new Map() this._fileEvents = [] this._fileEventDelayer = new Delayer(delayTime) this._ignoredRegistrations = new Set() this._onStop = undefined this._stateChangeEmitter = new Emitter() this._trace = Trace.Off this._tracer = { log: (messageOrDataObject: string | any, data?: string) => { if (Is.string(messageOrDataObject)) { this.traceMessage(messageOrDataObject, data) } else { this.traceObject(messageOrDataObject) } } } this._c2p = c2p.createConverter(clientOptions.uriConverter ? clientOptions.uriConverter.code2Protocol : undefined) this._syncedDocuments = new Map() this.registerBuiltinFeatures() Error.captureStackTrace(this) } public switchConsole(): void { this._consoleDebug = !this._consoleDebug this.changeTrace(Trace.Verbose, TraceFormat.Text) } private resolveClientOptions(clientOptions: LanguageClientOptions): ResolvedClientOptions { const markdown = { isTrusted: false, supportHtml: false } if (clientOptions.markdown != null) { markdown.isTrusted = clientOptions.markdown.isTrusted === true markdown.supportHtml = clientOptions.markdown.supportHtml === true } let disableSnippetCompletion = clientOptions.disableSnippetCompletion let disableMarkdown = clientOptions.disableMarkdown if (disableMarkdown === undefined) { disableMarkdown = workspace.initialConfiguration.get('coc.preferences.enableMarkdown') === false } const pullConfig = workspace.getConfiguration('pullDiagnostic', clientOptions.workspaceFolder) let pullOption = clientOptions.diagnosticPullOptions ?? {} if (pullOption.onChange === undefined) pullOption.onChange = pullConfig.get('onChange') if (pullOption.onSave === undefined) pullOption.onSave = pullConfig.get('onSave') if (pullOption.workspace === undefined) pullOption.workspace = pullConfig.get('workspace') pullOption.ignored = pullConfig.get('ignored', []).concat(pullOption.ignored ?? []) let disabledFeatures = clientOptions.disabledFeatures ?? [] for (let key of ['disableCompletion', 'disableWorkspaceFolders', 'disableDiagnostics']) { if (typeof clientOptions[key] === 'boolean') { let stack = '\n' + Error().stack.split('\n').slice(2, 4).join('\n') logger.warn(`${key} in the client options is deprecated. use disabledFeatures instead.`, stack) if (clientOptions[key] === true) { let s = key.slice(7) disabledFeatures.push(s[0].toLowerCase() + s.slice(1)) } } } return { disabledFeatures, disableMarkdown, disableSnippetCompletion, diagnosticPullOptions: pullOption, rootPatterns: defaultValue(clientOptions.rootPatterns, []), requireRootPattern: clientOptions.requireRootPattern, disableDynamicRegister: clientOptions.disableDynamicRegister, formatterPriority: defaultValue(clientOptions.formatterPriority, 0), ignoredRootPaths: defaultValue(clientOptions.ignoredRootPaths, []), documentSelector: defaultValue(clientOptions.documentSelector, []), synchronize: defaultValue(clientOptions.synchronize, {}), diagnosticCollectionName: clientOptions.diagnosticCollectionName, outputChannelName: defaultValue(clientOptions.outputChannelName, this._id), revealOutputChannelOn: defaultValue(clientOptions.revealOutputChannelOn, RevealOutputChannelOn.Never), stdioEncoding: defaultValue(clientOptions.stdioEncoding, 'utf8'), initializationOptions: clientOptions.initializationOptions, initializationFailedHandler: clientOptions.initializationFailedHandler, progressOnInitialization: clientOptions.progressOnInitialization === true, errorHandler: clientOptions.errorHandler ?? this.createDefaultErrorHandler(clientOptions.connectionOptions?.maxRestartCount), middleware: defaultValue(clientOptions.middleware, {}), workspaceFolder: clientOptions.workspaceFolder, connectionOptions: clientOptions.connectionOptions, uriConverter: clientOptions.uriConverter, textSynchronization: this.createTextSynchronizationOptions(clientOptions.textSynchronization), markdown } } private createTextSynchronizationOptions(options: LanguageClientOptions['textSynchronization']): ResolvedClientOptions['textSynchronization'] { if (options && typeof options.delayOpenNotifications === 'boolean') { return { delayOpenNotifications: options.delayOpenNotifications } } return { delayOpenNotifications: false } } public get supportedMarkupKind(): MarkupKind[] { if (!this.clientOptions.disableMarkdown) return [MarkupKind.Markdown, MarkupKind.PlainText] return [MarkupKind.PlainText] } public get state(): State { return this.getPublicState() } private get $state(): ClientState { return this._state } private set $state(value: ClientState) { let oldState = this.getPublicState() this._state = value let newState = this.getPublicState() if (newState !== oldState) { this._stateChangeEmitter.fire({ oldState, newState }) } } public get id(): string { return this._id } public get name(): string { return this._name } public get middleware(): Middleware { return this._clientOptions.middleware } public get code2ProtocolConverter(): c2p.Converter { return this._c2p } public getPublicState(): State { switch (this.$state) { case ClientState.Starting: return State.Starting case ClientState.Running: return State.Running case ClientState.StartFailed: return State.StartFailed default: return State.Stopped } } public get initializeResult(): InitializeResult | undefined { return this._initializeResult } public sendRequest(type: ProtocolRequestType0, token?: CancellationToken): Promise public sendRequest(type: ProtocolRequestType, params: NoInfer>, token?: CancellationToken): Promise public sendRequest(type: RequestType0, token?: CancellationToken): Promise public sendRequest(type: RequestType, params: NoInfer>, token?: CancellationToken): Promise public sendRequest(method: string, token?: CancellationToken): Promise public sendRequest(method: string, param: any, token?: CancellationToken): Promise public async sendRequest(type: string | MessageSignature, ...params: any[]): Promise { if (this.$state === ClientState.StartFailed || this.$state === ClientState.Stopping || this.$state === ClientState.Stopped) { return Promise.reject(new ResponseError(ErrorCodes.ConnectionInactive, `Client is not running`)) } const connection = await this.$start() // Send only depending open notifications await this._didOpenTextDocumentFeature!.sendPendingOpenNotifications() let param: any | undefined let token: CancellationToken | undefined // Separate cancellation tokens from other parameters for a better client interface if (params.length === 1) { // CancellationToken is an interface, so we need to check if the first param complies to it if (CancellationToken.is(params[0])) { token = params[0] } else { param = params[0] } } else if (params.length === 2) { param = params[0] token = params[1] } if (token !== undefined && token.isCancellationRequested) { return Promise.reject(new ResponseError(LSPErrorCodes.RequestCancelled, 'Request got cancelled')) } type = fixRequestType(type, params) const _sendRequest = this._clientOptions.middleware.sendRequest if (_sendRequest !== undefined) { // Return the general middleware invocation defining `next` as a utility function that reorganizes parameters to // pass them to the original sendRequest function. return _sendRequest(type, param, token, (type, param, token) => { const params: any[] = [] // Add the parameters if there are any if (param !== undefined) { params.push(param) } // Add the cancellation token if there is one if (token !== undefined) { params.push(token) } return connection.sendRequest(type, ...params) }) } else { return connection.sendRequest(type, ...params) } } public onRequest(type: ProtocolRequestType0, handler: NoInfer>): Disposable public onRequest(type: ProtocolRequestType, handler: NoInfer>): Disposable public onRequest(type: RequestType0, handler: NoInfer>): Disposable public onRequest(type: RequestType, handler: NoInfer>): Disposable public onRequest(method: string, handler: GenericRequestHandler): Disposable public onRequest(type: string | MessageSignature, handler: GenericRequestHandler): Disposable { const method = toMethod(type) this._requestHandlers.set(method, handler) const connection = this.activeConnection() let disposable: Disposable if (connection !== undefined) { this._requestDisposables.set(method, connection.onRequest(type, handler)) disposable = { dispose: () => { const disposable = this._requestDisposables.get(method) if (disposable !== undefined) { disposable.dispose() this._requestDisposables.delete(method) } } } } else { this._pendingRequestHandlers.set(method, handler) disposable = { dispose: () => { this._pendingRequestHandlers.delete(method) const disposable = this._requestDisposables.get(method) if (disposable !== undefined) { disposable.dispose() this._requestDisposables.delete(method) } } } } return { dispose: () => { this._requestHandlers.delete(method) disposable.dispose() } } } public sendNotification(type: ProtocolNotificationType0): Promise public sendNotification(type: ProtocolNotificationType, params?: NoInfer>): Promise public sendNotification(type: NotificationType0): Promise public sendNotification

(type: NotificationType

, params?: NoInfer>): Promise public sendNotification(method: string, params?: any): Promise public async sendNotification

(type: string | MessageSignature, params?: P): Promise { if (this.$state === ClientState.StartFailed || this.$state === ClientState.Stopping || this.$state === ClientState.Stopped) { // not throw for notification this.error(`Client is not running when send notification`, type) return } try { let documentToClose: string | undefined if (typeof type !== 'string' && type.method === DidCloseTextDocumentNotification.method) { documentToClose = (params as DidCloseTextDocumentParams).textDocument.uri } const connection = await this.$start() // Send any depending open notifications const didDropOpenNotification = await this._didOpenTextDocumentFeature!.sendPendingOpenNotifications(documentToClose) if (didDropOpenNotification) { // Don't forward this close notification if we dropped the // corresponding open notification. return } type = fixNotificationType(type, params == null ? [] : [params]) const _sendNotification = this._clientOptions.middleware.sendNotification return await Promise.resolve(_sendNotification ? _sendNotification(type, connection.sendNotification.bind(connection), params) : connection.sendNotification(type, params)) } catch (error) { this.error(`Sending notification ${toMethod(type)} failed.`, error) if ([ClientState.Stopping, ClientState.Stopped].includes(this._state)) return throw error } } public onNotification(type: ProtocolNotificationType0, handler: NotificationHandler0): Disposable public onNotification(type: ProtocolNotificationType, handler: NoInfer>): Disposable public onNotification(type: NotificationType0, handler: NotificationHandler0): Disposable public onNotification

(type: NotificationType

, handler: NoInfer>): Disposable public onNotification(method: string, handler: GenericNotificationHandler): Disposable public onNotification(type: string | MessageSignature, handler: GenericNotificationHandler): Disposable { const method = toMethod(type) this._notificationHandlers.set(method, handler) const connection = this.activeConnection() let disposable: Disposable if (connection !== undefined) { this._notificationDisposables.set(method, connection.onNotification(type, handler)) disposable = { dispose: () => { const disposable = this._notificationDisposables.get(method) if (disposable !== undefined) { disposable.dispose() this._notificationDisposables.delete(method) } } } } else { this._pendingNotificationHandlers.set(method, handler) disposable = { dispose: () => { this._pendingNotificationHandlers.delete(method) const disposable = this._notificationDisposables.get(method) if (disposable !== undefined) { disposable.dispose() this._notificationDisposables.delete(method) } } } } return { dispose: () => { this._notificationHandlers.delete(method) disposable.dispose() } } } public onProgress

(type: ProgressType, token: string | number, handler: NoInfer>): Disposable { this._progressHandlers.set(token, { type, handler }) const connection = this.activeConnection() let disposable: Disposable const handleWorkDoneProgress = this._clientOptions.middleware.handleWorkDoneProgress const realHandler = WorkDoneProgress.is(type) && handleWorkDoneProgress !== undefined ? (params: P) => { handleWorkDoneProgress(token, params as any, () => handler(params as unknown as P)) } : handler if (connection !== undefined) { this._progressDisposables.set(token, connection.onProgress(type, token, realHandler)) disposable = { dispose: () => { const disposable = this._progressDisposables.get(token) if (disposable !== undefined) { disposable.dispose() this._progressDisposables.delete(token) } } } } else { this._pendingProgressHandlers.set(token, { type, handler }) disposable = { dispose: () => { this._pendingProgressHandlers.delete(token) const disposable = this._progressDisposables.get(token) if (disposable !== undefined) { disposable.dispose() this._progressDisposables.delete(token) } } } } return { dispose: (): void => { this._progressHandlers.delete(token) disposable.dispose() } } } public async sendProgress

(type: ProgressType

, token: string | number, value: NoInfer>): Promise { if (this.$state === ClientState.StartFailed || this.$state === ClientState.Stopping || this.$state === ClientState.Stopped) { return Promise.reject(new ResponseError(ErrorCodes.ConnectionInactive, `Client is not running`)) } try { const connection = await this.$start() await connection.sendProgress(type, token, value) } catch (error) { this.error(`Sending progress for token ${token} failed.`, error) throw error } } /** * languageserver.xxx.settings or undefined */ public get configuredSection(): string | undefined { let section = defaultValue(this._clientOptions.synchronize, {}).configurationSection return typeof section === 'string' && section.startsWith('languageserver.') ? section : undefined } public get clientOptions(): ResolvedClientOptions { return this._clientOptions } public get onDidChangeState(): Event { return this._stateChangeEmitter.event } public get outputChannel(): OutputChannel { if (!this._outputChannel) { let { outputChannelName } = this._clientOptions this._outputChannel = window.createOutputChannel(defaultValue(outputChannelName, this._name)) } return this._outputChannel } public get traceOutputChannel(): OutputChannel { return this._traceOutputChannel ? this._traceOutputChannel : this.outputChannel } public get diagnostics(): DiagnosticCollection | undefined { return this._diagnostics } public createDefaultErrorHandler(maxRestartCount?: number): ErrorHandler { return new DefaultErrorHandler(this._id, maxRestartCount ?? 4, this._outputChannel) } public set trace(value: Trace) { this.changeTrace(value, this._traceFormat) } private consoleMessage(message: string, error = false): void { if (this._consoleDebug) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions error ? console.error(redOpen + message + redClose) : console.log(message) } } public debug(message: string, data?: any, showNotification = true): void { this.logOutputMessage(MessageType.Debug, RevealOutputChannelOn.Debug, 'Debug', message, data, showNotification) } public info(message: string, data?: any, showNotification = true): void { this.logOutputMessage(MessageType.Info, RevealOutputChannelOn.Info, 'Info', message, data, showNotification) } public warn(message: string, data?: any, showNotification = true): void { this.logOutputMessage(MessageType.Warning, RevealOutputChannelOn.Warn, 'Warn', message, data, showNotification) } public error(message: string, data?: any, showNotification: boolean | 'force' = true): void { this.logOutputMessage(MessageType.Error, RevealOutputChannelOn.Error, 'Error', message, data, showNotification) } private logOutputMessage(type: MessageType, reveal: RevealOutputChannelOn, name: string, message: string, data: any | undefined, showNotification: boolean | 'force'): void { const msg = `[${name.padEnd(5)} - ${currentTimeStamp()}] ${this.getLogMessage(message, data)}` this.outputChannel.appendLine(msg) this.consoleMessage(msg, type === MessageType.Error) if (showNotification === 'force' || (showNotification && this._clientOptions.revealOutputChannelOn <= reveal)) { this.showNotificationMessage(type, message, data) } } private traceObject(data: any): void { this.traceOutputChannel.appendLine(`${getTracePrefix(data)}${data2String(data)}`) } public traceMessage(message: string, data?: any): void { const msg = `[Trace - ${currentTimeStamp()}] ${this.getLogMessage(message, data)}` this.traceOutputChannel.appendLine(msg) this.consoleMessage(msg) } private getLogMessage(message: string, data?: any): string { return data != null ? `${message}\n${data2String(data)}` : message } private showNotificationMessage(type: MessageType, message?: string, data?: any) { message = message ?? 'A request has failed. See the output for more information.' if (data) { message += '\n' + data2String(data) } const messageFunc = type === MessageType.Error ? window.showErrorMessage.bind(window) : type === MessageType.Warning ? window.showWarningMessage.bind(window) : window.showInformationMessage.bind(window) let fn = getConditionValue(messageFunc, (_, _obj) => Promise.resolve(global.__showOutput)) fn(message, { title: 'Go to output' }).then(selection => { if (selection !== undefined) { this.outputChannel.show(true) } }, onUnexpectedError) } public needsStart(): boolean { return ( this.$state === ClientState.Initial || this.$state === ClientState.Stopping || this.$state === ClientState.Stopped ) } public needsStop(): boolean { return ( this.$state === ClientState.Starting || this.$state === ClientState.Running ) } private activeConnection(): Connection | undefined { return this.$state === ClientState.Running && this._connection !== undefined ? this._connection : undefined } public get hasPendingResponse(): boolean { return this._connection?.hasPendingResponse() } public onReady(): Promise { if (this._onStart) return this._onStart return new Promise(resolve => { let disposable = this.onDidChangeState(e => { if (e.newState === State.Running) { disposable.dispose() resolve() } }) }) } public get started(): boolean { return this.$state != ClientState.Initial } public isRunning(): boolean { return this.$state === ClientState.Running } public async _start(): Promise { if (this._disposed === 'disposing' || this._disposed === 'disposed') { throw new Error(`Client got disposed and can't be restarted.`) } if (this.$state === ClientState.Stopping) { throw new Error(`Client is currently stopping. Can only restart a full stopped client`) } // We are already running or are in the process of getting up // to speed. if (this._onStart !== undefined) { return this._onStart } this._rootPath = this.resolveRootPath() const [promise, resolve, reject] = this.createOnStartPromise() this._onStart = promise // If we restart then the diagnostics collection is reused. if (this._diagnostics === undefined) { let opts = this._clientOptions let name = opts.diagnosticCollectionName ? opts.diagnosticCollectionName : this._id if (!opts.disabledFeatures.includes('diagnostics')) { this._diagnostics = languages.createDiagnosticCollection(name) } } // When we start make all buffer handlers pending so that they // get added. for (const [method, handler] of this._notificationHandlers) { if (!this._pendingNotificationHandlers.has(method)) { this._pendingNotificationHandlers.set(method, handler) } } for (const [method, handler] of this._requestHandlers) { if (!this._pendingRequestHandlers.has(method)) { this._pendingRequestHandlers.set(method, handler) } } for (const [token, data] of this._progressHandlers) { if (!this._pendingProgressHandlers.has(token)) { this._pendingProgressHandlers.set(token, data) } } this.$state = ClientState.Starting try { const connection = await this.createConnection() this.handleConnectionEvents(connection) connection.listen() await this.initialize(connection) resolve() } catch (error) { this.$state = ClientState.StartFailed this.error(`${this._name} client: couldn't create connection to server.`, error, 'force') reject(error) } return this._onStart } public start(): Promise & Disposable { let p: any = this._start() p.dispose = () => { if (this.needsStop()) { void this.stop() } } return p } private async $start(): Promise { if (this.$state === ClientState.StartFailed) { throw new Error(`Previous start failed. Can't restart server.`) } await this._start() const connection = this.activeConnection() if (connection === undefined) { throw new Error(`Starting server failed`) } return connection } private handleConnectionEvents(connection: Connection) { connection.onNotification(LogMessageNotification.type, message => { switch (message.type) { case MessageType.Error: this.error(message.message) break case MessageType.Warning: this.warn(message.message) break case MessageType.Info: this.info(message.message) break case MessageType.Debug: this.debug(message.message) break default: this.outputChannel.appendLine(message.message) } }) connection.onNotification(ShowMessageNotification.type, message => { switch (message.type) { case MessageType.Error: void window.showErrorMessage(message.message) break case MessageType.Warning: void window.showWarningMessage(message.message) break case MessageType.Info: void window.showInformationMessage(message.message) break default: void window.showInformationMessage(message.message) } }) // connection.onNotification(TelemetryEventNotification.type, data => { // // Not supported. // // this._telemetryEmitter.fire(data); // }) connection.onRequest(ShowMessageRequest.type, (params: ShowMessageRequestParams) => { let messageFunc: (message: string, ...items: T[]) => Thenable switch (params.type) { case MessageType.Error: messageFunc = window.showErrorMessage.bind(window) break case MessageType.Warning: messageFunc = window.showWarningMessage.bind(window) break case MessageType.Info: messageFunc = window.showInformationMessage.bind(window) break default: messageFunc = window.showInformationMessage.bind(window) } let actions: MessageActionItem[] = toArray(params.actions) return messageFunc(params.message, ...actions) }) connection.onRequest(ShowDocumentRequest.type, async (params, token) => { const showDocument = async (params: ShowDocumentParams): Promise => { try { if (params.external === true || /^https?:\/\//.test(params.uri)) { await workspace.openResource(params.uri) return { success: true } } else { let { selection, takeFocus } = params if (takeFocus === false) { await workspace.loadFile(params.uri) } else { await workspace.jumpTo(params.uri, selection?.start) if (selection && comparePosition(selection.start, selection.end) != 0) { await window.selectRange(selection) } } return { success: true } } } catch (error) { return { success: false } } } const middleware = this._clientOptions.middleware.window?.showDocument if (middleware !== undefined) { return middleware(params, token, showDocument) } else { return showDocument(params) } }) } private createOnStartPromise(): [Promise, () => void, (error: any) => void] { let resolve!: () => void let reject!: (error: any) => void const promise: Promise = new Promise((_resolve, _reject) => { resolve = _resolve reject = _reject }) return [promise, resolve, reject] } private resolveRootPath(): string | null { if (this._clientOptions.workspaceFolder) { return URI.parse(this._clientOptions.workspaceFolder.uri).fsPath } let { ignoredRootPaths, rootPatterns, requireRootPattern } = this._clientOptions let resolved: string | undefined if (!isFalsyOrEmpty(rootPatterns)) { resolved = workspace.documentsManager.resolveRoot(rootPatterns, requireRootPattern) } let rootPath = resolved || workspace.rootPath if (sameFile(rootPath, os.homedir()) || ignoredRootPaths.some(p => sameFile(rootPath, p))) { this.warn(`Ignored rootPath ${rootPath} of client "${this._id}"`) return null } return rootPath } private initialize(connection: Connection): Promise { let { initializationOptions, workspaceFolder, progressOnInitialization } = this._clientOptions this.refreshTrace(false) let rootPath = this._rootPath let initParams: InitializeParams = { processId: process.pid, rootPath: rootPath ? rootPath : null, rootUri: rootPath ? this.code2ProtocolConverter.asUri(URI.file(rootPath)) : null, capabilities: this.computeClientCapabilities(), initializationOptions: Is.func(initializationOptions) ? initializationOptions() : initializationOptions, trace: Trace.toString(this._trace), workspaceFolders: workspaceFolder ? [workspaceFolder] : null, locale: getLocale(), clientInfo: { name: 'coc.nvim', version: workspace.version } } this.fillInitializeParams(initParams) if (progressOnInitialization) { const token: ProgressToken = UUID.generateUuid() initParams.workDoneToken = token connection.id = this._id const part = new ProgressPart(connection, token) part.begin({ title: `Initializing ${this.id}`, kind: 'begin' }) return this.doInitialize(connection, initParams).then(result => { part.done() return result }, (error: Error) => { part.done() return Promise.reject(error) }) } else { return this.doInitialize(connection, initParams) } } private async doInitialize(connection: Connection, initParams: InitializeParams): Promise { try { const result = await connection.initialize(initParams) if (result.capabilities.positionEncoding !== undefined && result.capabilities.positionEncoding !== PositionEncodingKind.UTF16) { throw new Error(`Unsupported position encoding (${result.capabilities.positionEncoding}) received from server ${this.name}`) } this._initializeResult = result this.$state = ClientState.Running let textDocumentSyncOptions: TextDocumentSyncOptions | undefined if (Is.number(result.capabilities.textDocumentSync)) { if (result.capabilities.textDocumentSync === TextDocumentSyncKind.None) { textDocumentSyncOptions = { openClose: false, change: TextDocumentSyncKind.None, save: undefined } } else { textDocumentSyncOptions = { openClose: true, change: result.capabilities.textDocumentSync, save: { includeText: false } } } } else if (result.capabilities.textDocumentSync !== undefined && result.capabilities.textDocumentSync !== null) { textDocumentSyncOptions = result.capabilities.textDocumentSync as TextDocumentSyncOptions } this._capabilities = Object.assign({}, result.capabilities, { resolvedTextDocumentSync: textDocumentSyncOptions }) connection.onNotification(PublishDiagnosticsNotification.type, params => this.handleDiagnostics(params)) for (let requestType of [RegistrationRequest.type, 'client/registerFeature']) { connection.onRequest(requestType, params => this.handleRegistrationRequest(params)) } for (let requestType of [UnregistrationRequest.type, 'client/unregisterFeature']) { connection.onRequest(requestType, params => this.handleUnregistrationRequest(params)) } connection.onRequest(ApplyWorkspaceEditRequest.type, params => this.handleApplyWorkspaceEdit(params)) // Add pending notification, request and progress handlers. for (const [method, handler] of this._pendingNotificationHandlers) { this._notificationDisposables.set(method, connection.onNotification(method, handler)) } this._pendingNotificationHandlers.clear() for (const [method, handler] of this._pendingRequestHandlers) { this._requestDisposables.set(method, connection.onRequest(method, handler)) } this._pendingRequestHandlers.clear() for (const [token, data] of this._pendingProgressHandlers) { this._progressDisposables.set(token, connection.onProgress(data.type, token, data.handler)) } this._pendingProgressHandlers.clear() await connection.sendNotification(InitializedNotification.type, {}) this.hookConfigurationChanged() this.initializeFeatures(connection) return result } catch (error: any) { this.error('Server initialization failed.', error) logger.error(`Server "${this.id}" initialization failed.`, error) let cb = (retry: boolean) => { process.nextTick(() => { new Promise((resolve, reject) => { if (retry) { this.initialize(connection).then(resolve, reject) } else { this.stop().then(resolve, reject) } }).catch(err => { this.error(`Unexpected error`, err, false) }) }) } if (this._clientOptions.initializationFailedHandler) { cb(this._clientOptions.initializationFailedHandler(error)) } else if (error instanceof ResponseError && error.data && error.data.retry) { void window.showErrorMessage(error.message, { title: 'Retry', id: 'retry' }).then(item => { cb(item && item.id === 'retry') }) } else { if (error && error.message) { void window.showErrorMessage(toText(error.message)) } cb(false) } throw error } } public stop(timeout = 2000): Promise { // Wait 2 seconds on stop return this.shutdown(ShutdownMode.Stop, timeout) } protected async shutdown(mode: ShutdownMode, timeout: number): Promise { // If the client is stopped or in its initial state return. if (this.$state === ClientState.Stopped || this.$state === ClientState.Initial) { return } if (this.$state === ClientState.Starting && this._onStart) { await this._onStart } // If we are stopping the client and have a stop promise return it. if (this.$state === ClientState.Stopping) { return this._onStop } const connection = this._connection // We can't stop a client that is not running (e.g. has no connection). Especially not // on that us starting since it can't be correctly synchronized. if (connection === undefined || (this.$state !== ClientState.Running && this.$state !== ClientState.StartFailed)) { throw new Error(`Client is not running and can't be stopped. Its current state is: ${this.$state}`) } this._initializeResult = undefined this.$state = ClientState.Stopping this.cleanUp(mode) let tm: NodeJS.Timeout const tp = new Promise(c => { tm = setTimeout(c, timeout) }) const shutdown = (async connection => { await connection.shutdown() await connection.exit() return connection })(connection) return this._onStop = Promise.race([tp, shutdown]).then(connection => { if (tm) clearTimeout(tm) // The connection won the race with the timeout. if (connection !== undefined) { connection.end() connection.dispose() } else { this.error(`Stopping server timed out`, undefined) throw new Error(`Stopping the server timed out`) } }, error => { this.error(`Stopping server failed`, error) throw error }).finally(() => { this.$state = ClientState.Stopped if (mode === 'stop') { this.cleanUpChannel() } this._onStart = undefined this._onStop = undefined this._connection = undefined this._ignoredRegistrations.clear() }) } public dispose(timeout = 2000): Promise { if (this._disposed) return try { this._disposed = 'disposing' if (!this.needsStop()) return return this.stop(timeout) } finally { this._disposed = 'disposed' } } private cleanUp(mode: ShutdownMode): void { this._fileEvents = [] this._fileEventDelayer.cancel() if (this._listeners) { disposeAll(this._listeners) } if (this._syncedDocuments) { this._syncedDocuments.clear() } // Clear features in reverse order; for (const feature of Array.from(this._features.entries()).map(entry => entry[1]).reverse()) { if (typeof feature.dispose === 'function') { feature.dispose() } } if ((mode === ShutdownMode.Stop || mode === ShutdownMode.Restart) && this._diagnostics !== undefined) { this._diagnostics.dispose() this._diagnostics = undefined } } private cleanUpChannel(): void { if (this._outputChannel) { this._outputChannel.dispose() this._outputChannel = undefined } } public notifyFileEvent(event: FileEvent | undefined): void { const didChangeWatchedFile = async (event: FileEvent | undefined): Promise => { if (event) this._fileEvents.push(event) return this._fileEventDelayer.trigger(async (): Promise => { const fileEvents = this._fileEvents if (fileEvents.length === 0) return this._fileEvents = [] try { await this.sendNotification(DidChangeWatchedFilesNotification.type, { changes: fileEvents }) } catch (error) { // Restore the file events. this._fileEvents = fileEvents throw error } }) } const workSpaceMiddleware = this.clientOptions.middleware.workspace; (workSpaceMiddleware?.didChangeWatchedFile ? workSpaceMiddleware.didChangeWatchedFile(event, didChangeWatchedFile) : didChangeWatchedFile(event)).catch(error => { this.error(`Notifying ${DidChangeWatchedFilesNotification.method} failed.`, error) }) } /** * @deprecated */ public async forceDocumentSync(): Promise { } public isSynced(uri: string): boolean { return this._syncedDocuments ? this._syncedDocuments.has(uri) : false } protected abstract createMessageTransports(encoding: string): Promise private async createConnection(): Promise { let onError = error => { this.error(`Unexpected connection error: `, error) } let errorHandler = (error: Error, message: Message | undefined, count: number | undefined) => { this.handleConnectionError(error, message, count).catch(onError) } let closeHandler = () => { this.handleConnectionClosed().catch(onError) } const transports = await this.createMessageTransports(defaultValue(this._clientOptions.stdioEncoding, 'utf8')) this._connection = createConnection(transports.reader, transports.writer, errorHandler, closeHandler, this._clientOptions.connectionOptions) return this._connection } protected async handleConnectionClosed(): Promise { // Check whether this is a normal shutdown in progress or the client stopped normally. if (this.$state === ClientState.Stopped) { logger.info(`client ${this._id} normal closed`) return } try { if (this._connection !== undefined) { this._connection.dispose() } } catch (error) { // Disposing a connection could fail if error cases. } let handlerResult: CloseHandlerResult = { action: CloseAction.DoNotRestart } let err if (this.$state !== ClientState.Stopping) { try { let result = await this._clientOptions.errorHandler.closed() handlerResult = toCloseHandlerResult(result) } catch (error) { err = error } } this._connection = undefined if (handlerResult.action === CloseAction.DoNotRestart) { this.error(handlerResult.message ?? 'Connection to server got closed. Server will not be restarted.', undefined, handlerResult.handled === true ? false : 'force') this.cleanUp(ShutdownMode.Stop) if (this.$state === ClientState.Starting) { this.$state = ClientState.StartFailed } else { this.$state = ClientState.Stopped } this._onStop = Promise.resolve() this._onStart = undefined } else if (handlerResult.action === CloseAction.Restart) { this.info(handlerResult.message ?? 'Connection to server got closed. Server will restart.', undefined, !handlerResult.handled) this.cleanUp(ShutdownMode.Restart) this.$state = ClientState.Initial this._onStop = Promise.resolve() this._onStart = undefined this.start().catch(error => { this.error(`Restarting server failed`, error, 'force') }) } if (err) throw err } public async handleConnectionError(error: Error, message: Message | undefined, count: number): Promise { let res = await this._clientOptions.errorHandler!.error(error, message, count) let result: ErrorHandlerResult = typeof res === 'number' ? { action: res } : defaultValue(res, { action: ErrorAction.Shutdown }) const showNotification = result.handled === true ? false : 'force' if (result.action === ErrorAction.Shutdown) { const msg = result.message ?? `Client ${this._name}: connection to server is erroring.\n${error.message}\nShutting down server.` this.error(msg, error, showNotification) return this.stop() } else { const msg = result.message ?? `Client ${this._name}: connection to server is erroring.\n${error.message}` this.error(msg, error, showNotification) } } private hookConfigurationChanged(): void { workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(this._id)) { this.refreshTrace(true) } }, null, this._listeners) } private refreshTrace(sendNotification: boolean): void { let config = workspace.getConfiguration(this._id, null) let trace: Trace = Trace.Off let traceFormat: TraceFormat = TraceFormat.Text if (config) { const traceConfig = config.get('trace.server', 'off') if (typeof traceConfig === 'string') { trace = Trace.fromString(traceConfig) } else { trace = Trace.fromString(config.get('trace.server.verbosity', 'off')) traceFormat = TraceFormat.fromString(config.get('trace.server.format', 'text')) } } if (sendNotification && this._trace == trace && this._traceFormat == traceFormat) { return } this.changeTrace(trace, traceFormat, sendNotification) } private changeTrace(trace: Trace, traceFormat: TraceFormat, sendNotification = true): void { this._trace = trace this._traceFormat = traceFormat if (this._connection && (this.$state === ClientState.Running || this.$state === ClientState.Starting)) { this._connection.trace(this._trace, this._tracer, { sendNotification, traceFormat: this._traceFormat }).catch(error => { this.error(`Updating trace failed with error`, error, false) }) } } private readonly _features: (StaticFeature | DynamicFeature)[] = [] private readonly _dynamicFeatures: Map> = new Map< string, DynamicFeature >() public registerFeatures( features: (StaticFeature | DynamicFeature)[] ): void { for (let feature of features) { this.registerFeature(feature, '') } } public registerFeature(feature: StaticFeature | DynamicFeature, name: string): void { let { disabledFeatures } = this._clientOptions if (disabledFeatures.length > 0 && disabledFeatures.includes(name)) return this._features.push(feature) if (DynamicFeature.is(feature)) { const registrationType = feature.registrationType this._dynamicFeatures.set(registrationType.method, feature) } } public getStaticFeature(method: typeof ConfigurationRequest.method): PullConfigurationFeature public getStaticFeature(method: typeof WorkDoneProgressCreateRequest.method): ProgressFeature public getStaticFeature(method: string): StaticFeature | undefined { return this._features.find(o => StaticFeature.is(o) && o.method == method) as StaticFeature } public getFeature(request: typeof ExecuteCommandRequest.method): DynamicFeature public getFeature(request: typeof DidChangeWorkspaceFoldersNotification.method): DynamicFeature public getFeature(request: typeof DidChangeWatchedFilesNotification.method): DynamicFeature public getFeature(request: typeof DidChangeConfigurationNotification.method): DynamicFeature public getFeature(request: typeof DidOpenTextDocumentNotification.method): DidOpenTextDocumentFeatureShape public getFeature(request: typeof DidChangeTextDocumentNotification.method): DidChangeTextDocumentFeatureShape public getFeature(request: typeof WillSaveTextDocumentNotification.method): DynamicFeature & TextDocumentSendFeature<(textDocument: TextDocumentWillSaveEvent) => Promise> public getFeature(request: typeof WillSaveTextDocumentWaitUntilRequest.method): DynamicFeature & TextDocumentSendFeature<(textDocument: TextDocument) => ProviderResult> public getFeature(request: typeof DidSaveTextDocumentNotification.method): DidSaveTextDocumentFeatureShape public getFeature(request: typeof DidCloseTextDocumentNotification.method): DidCloseTextDocumentFeatureShape public getFeature(request: typeof DidCreateFilesNotification.method): DynamicFeature & { send: (event: FileCreateEvent) => Promise } public getFeature(request: typeof DidRenameFilesNotification.method): DynamicFeature & { send: (event: FileRenameEvent) => Promise } public getFeature(request: typeof DidDeleteFilesNotification.method): DynamicFeature & { send: (event: FileDeleteEvent) => Promise } public getFeature(request: typeof WillCreateFilesRequest.method): DynamicFeature & { send: (event: FileWillCreateEvent) => Promise } public getFeature(request: typeof WillRenameFilesRequest.method): DynamicFeature & { send: (event: FileWillRenameEvent) => Promise } public getFeature(request: typeof WillDeleteFilesRequest.method): DynamicFeature & { send: (event: FileWillDeleteEvent) => Promise } public getFeature(request: typeof CompletionRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof HoverRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof SignatureHelpRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DefinitionRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof ReferencesRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentHighlightRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof CodeActionRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof CodeLensRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentFormattingRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentRangeFormattingRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentOnTypeFormattingRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof RenameRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentSymbolRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentLinkRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DocumentColorRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof DeclarationRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof FoldingRangeRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof ImplementationRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof SelectionRangeRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof TypeDefinitionRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof CallHierarchyPrepareRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof SemanticTokensRegistrationType.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof LinkedEditingRangeRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof TypeHierarchyPrepareRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof InlineCompletionRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof InlineValueRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof InlayHintRequest.method): DynamicFeature & TextDocumentProviderFeature public getFeature(request: typeof TextDocumentContentRequest.method): DynamicFeature & WorkspaceProviderFeature public getFeature(request: typeof WorkspaceSymbolRequest.method): DynamicFeature & WorkspaceProviderFeature public getFeature(request: typeof DocumentDiagnosticRequest.method): DynamicFeature & TextDocumentProviderFeature & DiagnosticFeatureShape public getFeature(request: string): DynamicFeature | undefined { return this._dynamicFeatures.get(request) } protected registerBuiltinFeatures() { this.registerFeature(new SyncConfigurationFeature(this), 'configuration') this._didOpenTextDocumentFeature = new DidOpenTextDocumentFeature(this, this._syncedDocuments) this.registerFeature(this._didOpenTextDocumentFeature, 'document') this.registerFeature(new DidChangeTextDocumentFeature(this), 'document') this.registerFeature(new DidCloseTextDocumentFeature(this, this._syncedDocuments), 'document') this.registerFeature(new WillSaveFeature(this), 'willSave') this.registerFeature(new WillSaveWaitUntilFeature(this), 'willSaveWaitUntil') this.registerFeature(new DidSaveTextDocumentFeature(this), 'didSave') this.registerFeature(new FileSystemWatcherFeature(this, this.notifyFileEvent.bind(this)), 'fileSystemWatcher') this.registerFeature(new CompletionItemFeature(this), 'completion') this.registerFeature(new HoverFeature(this), 'hover') this.registerFeature(new SignatureHelpFeature(this), 'signatureHelp') this.registerFeature(new ReferencesFeature(this), 'references') this.registerFeature(new DefinitionFeature(this), 'definition') this.registerFeature(new DocumentHighlightFeature(this), 'documentHighlight') this.registerFeature(new DocumentSymbolFeature(this), 'documentSymbol') this.registerFeature(new CodeActionFeature(this), 'codeAction') this.registerFeature(new CodeLensFeature(this), 'codeLens') this.registerFeature(new DocumentFormattingFeature(this), 'documentFormatting') this.registerFeature(new DocumentRangeFormattingFeature(this), 'documentRangeFormatting') this.registerFeature(new DocumentOnTypeFormattingFeature(this), 'documentOnTypeFormatting') this.registerFeature(new RenameFeature(this), 'rename') this.registerFeature(new DocumentLinkFeature(this), 'documentLink') this.registerFeature(new ExecuteCommandFeature(this), 'executeCommand') this.registerFeature(new PullConfigurationFeature(this), 'pullConfiguration') this.registerFeature(new TypeDefinitionFeature(this), 'typeDefinition') this.registerFeature(new ImplementationFeature(this), 'implementation') this.registerFeature(new DeclarationFeature(this), 'declaration') this.registerFeature(new ColorProviderFeature(this), 'colorProvider') this.registerFeature(new FoldingRangeFeature(this), 'foldingRange') this.registerFeature(new SelectionRangeFeature(this), 'selectionRange') this.registerFeature(new CallHierarchyFeature(this), 'callHierarchy') this.registerFeature(new ProgressFeature(this), 'progress') this.registerFeature(new LinkedEditingFeature(this), 'linkedEditing') this.registerFeature(new DidCreateFilesFeature(this), 'fileEvents') this.registerFeature(new DidRenameFilesFeature(this), 'fileEvents') this.registerFeature(new DidDeleteFilesFeature(this), 'fileEvents') this.registerFeature(new WillCreateFilesFeature(this), 'fileEvents') this.registerFeature(new WillRenameFilesFeature(this), 'fileEvents') this.registerFeature(new WillDeleteFilesFeature(this), 'fileEvents') this.registerFeature(new SemanticTokensFeature(this), 'semanticTokens') this.registerFeature(new InlayHintsFeature(this), 'inlayHint') this.registerFeature(new InlineCompletionItemFeature(this), 'inlineCompletion') this.registerFeature(new TextDocumentContentFeature(this), 'textDocumentContent') this.registerFeature(new InlineValueFeature(this), 'inlineValue') this.registerFeature(new DiagnosticFeature(this), 'pullDiagnostic') this.registerFeature(new TypeHierarchyFeature(this), 'typeHierarchy') this.registerFeature(new WorkspaceSymbolFeature(this), 'workspaceSymbol') // We only register the workspace folder feature if the client is not locked // to a specific workspace folder. if (this.clientOptions.workspaceFolder === undefined) { this.registerFeature(new WorkspaceFoldersFeature(this), 'workspaceFolders') } } public registerProposedFeatures() { this.registerFeatures(ProposedFeatures.createAll(this)) } private fillInitializeParams(params: InitializeParams): void { for (let feature of this._features) { if (Is.func(feature.fillInitializeParams)) { feature.fillInitializeParams(params) } } } private computeClientCapabilities(): ClientCapabilities { const result: ClientCapabilities = {} ensure(result, 'workspace')!.applyEdit = true const workspaceEdit = ensure(ensure(result, 'workspace')!, 'workspaceEdit')! workspaceEdit.documentChanges = true workspaceEdit.resourceOperations = [ResourceOperationKind.Create, ResourceOperationKind.Rename, ResourceOperationKind.Delete] workspaceEdit.failureHandling = FailureHandlingKind.Undo workspaceEdit.normalizesLineEndings = true workspaceEdit.changeAnnotationSupport = { groupsOnLabel: false } workspaceEdit.metadataSupport = true workspaceEdit.snippetEditSupport = true const diagnostics = ensure(ensure(result, 'textDocument')!, 'publishDiagnostics')! diagnostics.relatedInformation = true diagnostics.versionSupport = true diagnostics.tagSupport = { valueSet: [DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated] } diagnostics.codeDescriptionSupport = true diagnostics.dataSupport = true const textDocumentFilter = ensure(ensure(result, 'textDocument')!, 'filters')! textDocumentFilter.relativePatternSupport = true const windowCapabilities = ensure(result, 'window')! const showMessage = ensure(windowCapabilities, 'showMessage')! showMessage.messageActionItem = { additionalPropertiesSupport: true } const showDocument = ensure(windowCapabilities, 'showDocument')! showDocument.support = true const generalCapabilities = ensure(result, 'general')! generalCapabilities.staleRequestSupport = { cancel: true, retryOnContentModified: Array.from(BaseLanguageClient.RequestsToCancelOnContentModified) } generalCapabilities.regularExpressions = { engine: 'ECMAScript', version: 'ES2020' } generalCapabilities.markdown = { parser: 'marked', version: '7.0.5' } generalCapabilities.positionEncodings = ['utf-16'] // Added in 3.17.0 if (this._clientOptions.markdown.supportHtml) { generalCapabilities.markdown.allowedTags = ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'span'] } for (let feature of this._features) { feature.fillClientCapabilities(result) } return result } private initializeFeatures(_connection: Connection): void { let documentSelector = this._clientOptions.documentSelector for (let feature of this._features) { if (Is.func(feature.preInitialize)) { feature.preInitialize(this._capabilities, documentSelector) } } for (let feature of this._features) { feature.initialize(this._capabilities, documentSelector) } } private handleRegistrationRequest(params: RegistrationParams): Promise { if (this.clientOptions.disableDynamicRegister) return const middleware = this.clientOptions.middleware.handleRegisterCapability if (middleware) { return middleware(params, nextParams => this.doRegisterCapability(nextParams)) } else { return this.doRegisterCapability(params) } } private async doRegisterCapability(params: RegistrationParams): Promise { // We will not receive a registration call before a client is running // from a server. However if we stop or shutdown we might which might // try to restart the server. So ignore registrations if we are not running if (!this.isRunning()) { for (const registration of params.registrations) { this._ignoredRegistrations.add(registration.id) } return } for (const registration of params.registrations) { const feature = this._dynamicFeatures.get(registration.method) if (!feature) { this.error(`No feature implementation for "${registration.method}" found. Registration failed.`, undefined, false) return } const options = defaultValue(registration.registerOptions, {}) options.documentSelector = options.documentSelector ?? this._clientOptions.documentSelector const data: RegistrationData = { id: registration.id, registerOptions: options } feature.register(data) } } private handleUnregistrationRequest(params: UnregistrationParams): Promise { const middleware = this._clientOptions.middleware.handleUnregisterCapability if (middleware) { return middleware(params, nextParams => this.doUnregisterCapability(nextParams)) } else { return this.doUnregisterCapability(params) } } private async doUnregisterCapability(params: UnregistrationParams): Promise { for (const unregistration of params.unregisterations) { if (this._ignoredRegistrations.has(unregistration.id)) { continue } const feature = this._dynamicFeatures.get(unregistration.method) if (feature) feature.unregister(unregistration.id) } } private handleDiagnostics(params: PublishDiagnosticsParams) { let { uri, diagnostics, version } = params if (Is.number(version) && !workspace.hasDocument(uri, version)) return let middleware = this.clientOptions.middleware!.handleDiagnostics if (middleware) { middleware(uri, diagnostics, (uri, diagnostics) => this.setDiagnostics(uri, diagnostics) ) } else { this.setDiagnostics(uri, diagnostics) } } private setDiagnostics(uri: string, diagnostics: Diagnostic[] | undefined) { if (!this._diagnostics) return this._diagnostics.set(uri, diagnostics) } private doHandleApplyWorkspaceEdit(params: ApplyWorkspaceEditParams): Promise { return workspace.applyEdit(params.edit).then(applied => { return { applied } }) } private async handleApplyWorkspaceEdit(params: ApplyWorkspaceEditParams): Promise { const middleware = this.clientOptions.middleware.workspace?.handleApplyEdit if (middleware) { try { let resultOrError = await Promise.resolve(middleware(params, nextParams => this.doHandleApplyWorkspaceEdit(nextParams))) if (resultOrError instanceof ResponseError) { throw resultOrError } } catch (error) { this.error(`Error on apply workspace edit`, error, false) return { applied: false } } } else { return this.doHandleApplyWorkspaceEdit(params) } } private static RequestsToCancelOnContentModified: Set = new Set([ InlayHintRequest.method, SemanticTokensRequest.method, SemanticTokensRangeRequest.method, SemanticTokensDeltaRequest.method ]) public handleFailedRequest(type: P, token: CancellationToken | undefined, error: any, defaultValue: T, showNotification = true): T { if (token && token.isCancellationRequested) return defaultValue // If we get a request cancel or a content modified don't log anything. if (error instanceof ResponseError) { // The connection got disposed while we were waiting for a response. // Simply return the default value. Is the best we can do. if (error.code === ErrorCodes.PendingResponseRejected || error.code === ErrorCodes.ConnectionInactive) { return defaultValue } if (error.code === LSPErrorCodes.RequestCancelled || error.code === LSPErrorCodes.ServerCancelled) { if (error.data != null) { throw new LSPCancellationError(error.data) } else { throw new CancellationError() } } else if (error.code === LSPErrorCodes.ContentModified) { if (BaseLanguageClient.RequestsToCancelOnContentModified.has(type.method)) { throw new CancellationError() } else { return defaultValue } } } this.error(`Request ${type.method} failed.`, error, showNotification) throw error } // Should be keeped public logFailedRequest(type: any, error: any): void { // If we get a request cancel don't log anything. if ( error instanceof ResponseError && error.code === LSPErrorCodes.RequestCancelled ) { return } this.error(`Request ${type.method} failed.`, error) } /** * Return extension name or id. */ public getExtensionName(): string { if (this.__extensionName) return this.__extensionName let name = parseExtensionName(toText(this['stack'])) if (name && name !== 'coc.nvim') { this.__extensionName = name return name } return this._id } /** * Add __extensionName property to provider */ public attachExtensionName(provider: T): void { if (!provider.hasOwnProperty('__extensionName')) { Object.defineProperty(provider, '__extensionName', { get: () => this.getExtensionName(), enumerable: true }) } } } const ProposedFeatures = { createAll: (_client: BaseLanguageClient): (StaticFeature | DynamicFeature)[] => { let result: (StaticFeature | DynamicFeature)[] = [] return result } } ================================================ FILE: src/language-client/codeAction.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, CodeAction, CodeActionContext, CodeActionOptions, CodeActionParams, CodeActionRegistrationOptions, Disposable, DocumentSelector, ExecuteCommandParams, Range, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import { CodeActionKind, Command } from 'vscode-languageserver-types' import commands from '../commands' import languages from '../languages' import { CodeActionProvider, ProviderResult } from '../provider' import { CodeActionRequest, CodeActionResolveRequest, ExecuteCommandRequest } from '../util/protocol' import { ExecuteCommandMiddleware, ExecuteCommandSignature } from './executeCommand' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideCodeActionsSignature { ( this: void, document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken ): ProviderResult<(Command | CodeAction)[]> } export interface ResolveCodeActionSignature { (this: void, item: CodeAction, token: CancellationToken): ProviderResult } export interface CodeActionMiddleware { provideCodeActions?: ( this: void, document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken, next: ProvideCodeActionsSignature ) => ProviderResult<(Command | CodeAction)[]> resolveCodeAction?: ( this: void, item: CodeAction, token: CancellationToken, next: ResolveCodeActionSignature ) => ProviderResult } export class CodeActionFeature extends TextDocumentLanguageFeature { private disposables: Disposable[] = [] constructor(client: FeatureClient) { super(client, CodeActionRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const cap = ensure(ensure(capabilities, 'textDocument')!, 'codeAction')! cap.dynamicRegistration = true cap.isPreferredSupport = true cap.disabledSupport = true cap.dataSupport = true cap.honorsChangeAnnotations = false cap.resolveSupport = { properties: ['edit'] } cap.codeActionLiteralSupport = { codeActionKind: { valueSet: [ CodeActionKind.Empty, CodeActionKind.QuickFix, CodeActionKind.Refactor, CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline, CodeActionKind.RefactorRewrite, CodeActionKind.Source, CodeActionKind.SourceOrganizeImports ] } } } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.codeActionProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: CodeActionRegistrationOptions ): [Disposable, CodeActionProvider] { const registerCommand = (id: string) => { const client = this._client const executeCommand: ExecuteCommandSignature = (command: string, args: any[]): any => { const params: ExecuteCommandParams = { command, arguments: args } return client.sendRequest(ExecuteCommandRequest.type, params) } this.disposables.push(commands.registerCommand(id, (...args: any[]) => { return this.sendWithMiddleware(executeCommand, 'executeCommand', id, args) }, null, true)) } const provider: CodeActionProvider = { provideCodeActions: (document, range, context, token) => { const client = this._client const _provideCodeActions: ProvideCodeActionsSignature = (document, range, context, token) => { const params: CodeActionParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range, context, } return this.sendRequest(CodeActionRequest.type, params, token).then( values => { if (!values) return undefined // some server may not registered commands to client. values.forEach(val => { let cmd = Command.is(val) ? val.command : val.command?.command if (cmd && !commands.has(cmd)) registerCommand(cmd) }) return values } ) } const middleware = client.middleware! return middleware.provideCodeActions ? middleware.provideCodeActions(document, range, context, token, _provideCodeActions) : _provideCodeActions(document, range, context, token) }, resolveCodeAction: options.resolveProvider ? (item: CodeAction, token: CancellationToken) => { const middleware = this._client.middleware! const resolveCodeAction: ResolveCodeActionSignature = (item, token) => { return this.sendRequest(CodeActionResolveRequest.type, item, token, item) } return middleware.resolveCodeAction ? middleware.resolveCodeAction(item, token, resolveCodeAction) : resolveCodeAction(item, token) } : undefined } this._client.attachExtensionName(provider) return [languages.registerCodeActionProvider(options.documentSelector, provider, this._client.id, options.codeActionKinds), provider] } public dispose(): void { this.disposables.forEach(o => { o.dispose() }) this.disposables = [] super.dispose() } } ================================================ FILE: src/language-client/codeLens.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, CodeLens, CodeLensOptions, CodeLensRegistrationOptions, Disposable, DocumentSelector, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { CodeLensProvider, ProviderResult } from '../provider' import { CodeLensRefreshRequest, CodeLensRequest, CodeLensResolveRequest, Emitter } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideCodeLensesSignature { (this: void, document: TextDocument, token: CancellationToken): ProviderResult } export interface ResolveCodeLensSignature { (this: void, codeLens: CodeLens, token: CancellationToken): ProviderResult } export interface CodeLensMiddleware { provideCodeLenses?: (this: void, document: TextDocument, token: CancellationToken, next: ProvideCodeLensesSignature) => ProviderResult resolveCodeLens?: (this: void, codeLens: CodeLens, token: CancellationToken, next: ResolveCodeLensSignature) => ProviderResult } export interface CodeLensProviderShape { provider?: CodeLensProvider onDidChangeCodeLensEmitter: Emitter } export class CodeLensFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, CodeLensRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'codeLens')!.dynamicRegistration = true ensure(ensure(capabilities, 'workspace')!, 'codeLens')!.refreshSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const client = this._client client.onRequest(CodeLensRefreshRequest.type, async () => { for (const provider of this.getAllProviders()) { provider.onDidChangeCodeLensEmitter.fire() } }) const options = this.getRegistrationOptions(documentSelector, capabilities.codeLensProvider) if (!options) return this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider(options: CodeLensRegistrationOptions): [Disposable, CodeLensProviderShape] { const emitter: Emitter = new Emitter() const provider: CodeLensProvider = { onDidChangeCodeLenses: emitter.event, provideCodeLenses: (document, token) => { const client = this._client const provideCodeLenses: ProvideCodeLensesSignature = (document, token) => { return this.sendRequest( CodeLensRequest.type, client.code2ProtocolConverter.asCodeLensParams(document), token ) } const middleware = client.middleware! return middleware.provideCodeLenses ? middleware.provideCodeLenses(document, token, provideCodeLenses) : provideCodeLenses(document, token) }, resolveCodeLens: (options.resolveProvider) ? (codeLens: CodeLens, token: CancellationToken): ProviderResult => { const client = this._client const resolveCodeLens: ResolveCodeLensSignature = (codeLens, token) => { return this.sendRequest( CodeLensResolveRequest.type, codeLens, token, codeLens ) } const middleware = client.middleware! return middleware.resolveCodeLens ? middleware.resolveCodeLens(codeLens, token, resolveCodeLens) : resolveCodeLens(codeLens, token) } : undefined } this._client.attachExtensionName(provider) return [languages.registerCodeLensProvider(options.documentSelector, provider), { provider, onDidChangeCodeLensEmitter: emitter }] } } ================================================ FILE: src/language-client/colorProvider.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Color, ColorInformation, ColorPresentation, ColorPresentationParams, Disposable, DocumentColorOptions, DocumentColorParams, DocumentColorRegistrationOptions, DocumentSelector, Range, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { DocumentColorProvider, ProviderResult } from '../provider' import { ColorPresentationRequest, DocumentColorRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export type ProvideDocumentColorsSignature = (document: TextDocument, token: CancellationToken) => ProviderResult export type ProvideColorPresentationSignature = ( color: Color, context: { document: TextDocument; range: Range }, token: CancellationToken ) => ProviderResult export interface ColorProviderMiddleware { provideDocumentColors?: ( this: void, document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature ) => ProviderResult provideColorPresentations?: ( this: void, color: Color, context: { document: TextDocument; range: Range }, token: CancellationToken, next: ProvideColorPresentationSignature ) => ProviderResult } export class ColorProviderFeature extends TextDocumentLanguageFeature< boolean | DocumentColorOptions, DocumentColorRegistrationOptions, DocumentColorProvider, ColorProviderMiddleware > { constructor(client: FeatureClient) { super(client, DocumentColorRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'colorProvider')!.dynamicRegistration = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { let [id, options] = this.getRegistration(documentSelector, capabilities.colorProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider( options: DocumentColorRegistrationOptions ): [Disposable, DocumentColorProvider] { const provider: DocumentColorProvider = { provideColorPresentations: (color, context, token) => { const client = this._client const provideColorPresentations: ProvideColorPresentationSignature = (color, context, token) => { const requestParams: ColorPresentationParams = { color, textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(context.document), range: context.range } return this.sendRequest(ColorPresentationRequest.type, requestParams, token) } const middleware = client.middleware return middleware.provideColorPresentations ? middleware.provideColorPresentations(color, context, token, provideColorPresentations) : provideColorPresentations(color, context, token) }, provideDocumentColors: (document, token) => { const client = this._client const provideDocumentColors: ProvideDocumentColorsSignature = (document, token) => { const requestParams: DocumentColorParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) } return this.sendRequest(DocumentColorRequest.type, requestParams, token) } const middleware = client.middleware return middleware.provideDocumentColors ? middleware.provideDocumentColors(document, token, provideDocumentColors) : provideDocumentColors(document, token) } } this._client.attachExtensionName(provider) return [languages.registerDocumentColorProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/completion.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, CompletionContext, CompletionOptions, CompletionRegistrationOptions, DocumentSelector, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { CompletionItem, CompletionItemKind, CompletionItemTag, CompletionList, InsertTextMode, Position } from 'vscode-languageserver-types' import languages from '../languages' import { CompletionItemProvider, ProviderResult } from '../provider' import { CompletionRequest, CompletionResolveRequest, Disposable } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' const SupportedCompletionItemKinds: CompletionItemKind[] = [ CompletionItemKind.Text, CompletionItemKind.Method, CompletionItemKind.Function, CompletionItemKind.Constructor, CompletionItemKind.Field, CompletionItemKind.Variable, CompletionItemKind.Class, CompletionItemKind.Interface, CompletionItemKind.Module, CompletionItemKind.Property, CompletionItemKind.Unit, CompletionItemKind.Value, CompletionItemKind.Enum, CompletionItemKind.Keyword, CompletionItemKind.Snippet, CompletionItemKind.Color, CompletionItemKind.File, CompletionItemKind.Reference, CompletionItemKind.Folder, CompletionItemKind.EnumMember, CompletionItemKind.Constant, CompletionItemKind.Struct, CompletionItemKind.Event, CompletionItemKind.Operator, CompletionItemKind.TypeParameter ] export interface ProvideCompletionItemsSignature { ( this: void, document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, ): ProviderResult } export interface ResolveCompletionItemSignature { (this: void, item: CompletionItem, token: CancellationToken): ProviderResult } export interface CompletionMiddleware { provideCompletionItem?: ( this: void, document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature ) => ProviderResult resolveCompletionItem?: ( this: void, item: CompletionItem, token: CancellationToken, next: ResolveCompletionItemSignature ) => ProviderResult } export interface $CompletionOptions { disableSnippetCompletion?: boolean } export class CompletionItemFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, CompletionRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let snippetSupport = this._client.clientOptions.disableSnippetCompletion !== true let completion = ensure(ensure(capabilities, 'textDocument')!, 'completion')! completion.dynamicRegistration = true completion.contextSupport = true completion.completionItem = { snippetSupport, commitCharactersSupport: true, documentationFormat: this._client.supportedMarkupKind, deprecatedSupport: true, preselectSupport: true, insertReplaceSupport: true, tagSupport: { valueSet: [CompletionItemTag.Deprecated] }, resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits'] }, labelDetailsSupport: true, insertTextModeSupport: { valueSet: [InsertTextMode.asIs, InsertTextMode.adjustIndentation] } } completion.completionItemKind = { valueSet: SupportedCompletionItemKinds } completion.insertTextMode = InsertTextMode.adjustIndentation completion.completionList = { itemDefaults: [ 'commitCharacters', 'editRange', 'insertTextFormat', 'insertTextMode' ] } } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.completionProvider) if (!options) return this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider(options: CompletionRegistrationOptions & { priority?: number }, id: string): [Disposable, CompletionItemProvider] { let triggerCharacters = options.triggerCharacters || [] let allCommitCharacters = options.allCommitCharacters || [] const provider: CompletionItemProvider = { provideCompletionItems: (document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult => { const middleware = this._client.middleware const provideCompletionItems: ProvideCompletionItemsSignature = (document, position, context, token) => { return this.sendRequest( CompletionRequest.type, this._client.code2ProtocolConverter.asCompletionParams(document, position, context), token, [] ) } return middleware.provideCompletionItem ? middleware.provideCompletionItem(document, position, context, token, provideCompletionItems) : provideCompletionItems(document, position, context, token) }, resolveCompletionItem: options.resolveProvider ? (item: CompletionItem, token: CancellationToken): ProviderResult => { const middleware = this._client.middleware! const resolveCompletionItem: ResolveCompletionItemSignature = (item, token) => { return this.sendRequest( CompletionResolveRequest.type, item, token, item ) } return middleware.resolveCompletionItem ? middleware.resolveCompletionItem(item, token, resolveCompletionItem) : resolveCompletionItem(item, token) } : undefined } this._client.attachExtensionName(provider) // index is needed since one language server could create many sources. let name = this._client.id + (this.registrationLength == 0 ? '' : '-' + id) const disposable = languages.registerCompletionItemProvider( name, 'LS', options.documentSelector, provider, triggerCharacters, options.priority, allCommitCharacters) return [disposable, provider] } } ================================================ FILE: src/language-client/configuration.ts ================================================ 'use strict' import type { ClientCapabilities, DidChangeConfigurationRegistrationOptions, Disposable, RegistrationType, WorkspaceFolder } from 'vscode-languageserver-protocol' import { IConfigurationChangeEvent, WorkspaceConfiguration } from '../configuration/types' import { mergeConfigProperties, toJSONObject } from '../configuration/util' import { IFileSystemWatcher } from '../types' import { defaultValue } from '../util' import * as Is from '../util/is' import { ConfigurationRequest, DidChangeConfigurationNotification } from '../util/protocol' import workspace from '../workspace' import { DynamicFeature, ensure, FeatureClient, FeatureState, RegistrationData, StaticFeature } from './features' import * as UUID from './utils/uuid' export interface ConfigurationMiddleware { configuration?: ConfigurationRequest.MiddlewareSignature } interface ConfigurationWorkspaceMiddleware { workspace?: ConfigurationMiddleware } export interface SynchronizeOptions { /** * The configuration sections to synchronize. Pushing settings from the * client to the server is deprecated in favour of the new pull model * that allows servers to query settings scoped on resources. In this * model the client can only deliver an empty change event since the * actually setting value can vary on the provided resource scope. * @deprecated Use the new pull model (`workspace/configuration` request) */ configurationSection?: string | string[] /** * Asks the client to send file change events to the server. Watchers * operate on workspace folders. The LSP client doesn't support watching * files outside a workspace folder. */ fileEvents?: IFileSystemWatcher | IFileSystemWatcher[] } export interface $ConfigurationOptions { synchronize?: SynchronizeOptions workspaceFolder?: WorkspaceFolder } export class PullConfigurationFeature implements StaticFeature { constructor(private _client: FeatureClient) { } public get method(): string { return ConfigurationRequest.method } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(capabilities, 'workspace').configuration = true } public getState(): FeatureState { return { kind: 'static' } } public initialize(): void { let client = this._client let { configuredSection } = client client.onRequest(ConfigurationRequest.type, (params, token) => { let configuration: ConfigurationRequest.HandlerSignature = params => { let result: any[] = [] for (let item of params.items) { let section = configuredSection ? configuredSection + (item.section ? `.${item.section}` : '') : item.section result.push(this.getConfiguration(item.scopeUri, section)) } return result } let middleware = client.middleware.workspace return middleware?.configuration ? middleware.configuration(params, token, configuration) : configuration(params, token) }) } private getConfiguration(resource: string | undefined, section: string | undefined): any { let result: any = null if (section) { let index = section.lastIndexOf('.') if (index === -1) { result = toJSONObject(workspace.getConfiguration(undefined, resource).get(section)) } else { let config = workspace.getConfiguration(section.substr(0, index), resource) result = toJSONObject(mergeConfigProperties(config))[section.substr(index + 1)] } } else { let config = workspace.getConfiguration(section, resource) result = {} for (let key of Object.keys(config)) { if (config.has(key)) { result[key] = toJSONObject(config.get(key)) } } } return result ?? null } public dispose(): void { } } export interface DidChangeConfigurationSignature { (this: void, sections: string[] | undefined): Promise } export interface DidChangeConfigurationMiddleware { didChangeConfiguration?: (this: void, sections: string[] | undefined, next: DidChangeConfigurationSignature) => Promise } interface DidChangeConfigurationWorkspaceMiddleware { workspace?: DidChangeConfigurationMiddleware } export class SyncConfigurationFeature implements DynamicFeature { private _listeners: Map = new Map() private configuredUID: string | undefined constructor(private _client: FeatureClient) {} public getState(): FeatureState { return { kind: 'workspace', id: this.registrationType.method, registrations: this._listeners.size > 0 } } public get registrationType(): RegistrationType { return DidChangeConfigurationNotification.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'workspace')!, 'didChangeConfiguration')!.dynamicRegistration = true } public initialize(): void { let section = defaultValue(this._client.clientOptions.synchronize, {}).configurationSection if (section !== undefined) { let id = this.configuredUID = UUID.generateUuid() this.register({ id, registerOptions: { section } }) } } public register( data: RegistrationData ): void { if (this._client.configuredSection && data.id !== this.configuredUID) return let { section } = data.registerOptions let disposable = workspace.onDidChangeConfiguration(event => { this.onDidChangeConfiguration(section, event) }) this._listeners.set(data.id, disposable) if (section !== undefined) { this.onDidChangeConfiguration(section, undefined) } } public unregister(id: string): void { let disposable = this._listeners.get(id) if (disposable) { this._listeners.delete(id) disposable.dispose() } } public dispose(): void { for (let disposable of this._listeners.values()) { disposable.dispose() } this._listeners.clear() } private onDidChangeConfiguration(configurationSection: string | string[] | undefined, event: IConfigurationChangeEvent | undefined): void { let { configuredSection } = this._client let sections: string[] | undefined if (Is.string(configurationSection)) { sections = [configurationSection] } else { sections = configurationSection } if (sections != null && event != null) { let keys = sections.map(s => s.startsWith('languageserver.') ? 'languageserver' : s) let affected = keys.some(section => event.affectsConfiguration(section)) if (!affected) return } let didChangeConfiguration = (sections: string[] | undefined): Promise => { if (sections == null) { return this._client.sendNotification(DidChangeConfigurationNotification.type, { settings: null }) } let workspaceFolder = this._client.clientOptions.workspaceFolder let settings = configuredSection ? SyncConfigurationFeature.getConfiguredSettings(configuredSection, workspaceFolder) : SyncConfigurationFeature.extractSettingsInformation(sections, workspaceFolder) return this._client.sendNotification(DidChangeConfigurationNotification.type, { settings }) } let middleware = this._client.middleware.workspace?.didChangeConfiguration let promise = middleware ? Promise.resolve(middleware(sections, didChangeConfiguration)) : didChangeConfiguration(sections) promise.catch(error => { this._client.error(`Sending notification ${DidChangeConfigurationNotification.type.method} failed`, error) }) } public static getConfiguredSettings(key: string, workspaceFolder: WorkspaceFolder | undefined): any { let len = '.settings'.length let config = workspace.getConfiguration(key.slice(0, - len), workspaceFolder) return mergeConfigProperties(toJSONObject(config.get('settings', {}))) } public static extractSettingsInformation(keys: string[], workspaceFolder?: WorkspaceFolder): any { function ensurePath(config: any, path: string[]): any { let current = config for (let i = 0; i < path.length - 1; i++) { let obj = current[path[i]] if (!obj) { obj = Object.create(null) current[path[i]] = obj } current = obj } return current } let result = Object.create(null) for (let i = 0; i < keys.length; i++) { let key = keys[i] let index: number = key.indexOf('.') let config: WorkspaceConfiguration if (index >= 0) { config = workspace.getConfiguration(key.substr(0, index), workspaceFolder).get(key.substr(index + 1)) } else { config = workspace.getConfiguration(key, workspaceFolder) } let path = keys[i].split('.') ensurePath(result, path)[path[path.length - 1]] = config } return result } } ================================================ FILE: src/language-client/declaration.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Declaration, DeclarationLink, DeclarationOptions, DeclarationRegistrationOptions, Disposable, DocumentSelector, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { DeclarationProvider, ProviderResult } from '../provider' import { DeclarationRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface ProvideDeclarationSignature { (this: void, document: TextDocument, position: Position, token: CancellationToken): ProviderResult } export interface DeclarationMiddleware { provideDeclaration?: (this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideDeclarationSignature) => ProviderResult } export class DeclarationFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, DeclarationRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let declarationSupport = ensure(ensure(capabilities, 'textDocument')!, 'declaration')! declarationSupport.dynamicRegistration = true declarationSupport.linkSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const [id, options] = this.getRegistration(documentSelector, capabilities.declarationProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: DeclarationRegistrationOptions): [Disposable, DeclarationProvider] { const provider: DeclarationProvider = { provideDeclaration: (document: TextDocument, position: Position, token: CancellationToken) => { const client = this._client const provideDeclaration: ProvideDeclarationSignature = (document, position, token) => this.sendRequest(DeclarationRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token) const middleware = client.middleware return middleware.provideDeclaration ? middleware.provideDeclaration(document, position, token, provideDeclaration) : provideDeclaration(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerDeclarationProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/definition.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Definition, DefinitionLink, DefinitionOptions, DefinitionRegistrationOptions, Disposable, DocumentSelector, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { DefinitionProvider, ProviderResult } from '../provider' import { DefinitionRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideDefinitionSignature { ( this: void, document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } export interface DefinitionMiddleware { provideDefinition?: ( this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideDefinitionSignature ) => ProviderResult } export class DefinitionFeature extends TextDocumentLanguageFeature< boolean | DefinitionOptions, DefinitionRegistrationOptions, DefinitionProvider, DefinitionMiddleware > { constructor(client: FeatureClient) { super(client, DefinitionRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let definitionSupport = ensure(ensure(capabilities, 'textDocument')!, 'definition')! definitionSupport.dynamicRegistration = true definitionSupport.linkSupport = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.definitionProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: DefinitionRegistrationOptions ): [Disposable, DefinitionProvider] { const provider: DefinitionProvider = { provideDefinition: (document, position, token) => { const client = this._client const provideDefinition: ProvideDefinitionSignature = (document, position, token) => { return this.sendRequest( DefinitionRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token ) } const middleware = client.middleware! return middleware.provideDefinition ? middleware.provideDefinition(document, position, token, provideDefinition) : provideDefinition(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerDefinitionProvider(options.documentSelector!, provider), provider] } } ================================================ FILE: src/language-client/diagnostic.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import type { CancellationToken, ClientCapabilities, Diagnostic, DiagnosticOptions, DiagnosticRegistrationOptions, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentSelector, PreviousResultId, ServerCapabilities, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, WorkspaceDiagnosticReportPartialResult } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { DiagnosticTag } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import DiagnosticCollection from '../diagnostic/collection' import languages from '../languages' import { DiagnosticProvider, ProviderResult, ResultReporter } from '../provider' import { TextDocumentMatch } from '../types' import { defaultValue, getConditionValue } from '../util' import { CancellationError } from '../util/errors' import { LinkedMap, Touch } from '../util/map' import { minimatch } from '../util/node' import { CancellationTokenSource, DiagnosticRefreshRequest, DiagnosticServerCancellationData, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, DidSaveTextDocumentNotification, Disposable, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, Emitter, RAL, WorkspaceDiagnosticRequest } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { BaseFeature, ensure, FeatureClient, LSPCancellationError, TextDocumentLanguageFeature } from './features' interface HandleDiagnosticsSignature { (this: void, uri: string, diagnostics: Diagnostic[]): void } export type ProvideDiagnosticSignature = (this: void, document: TextDocument | URI, previousResultId: string | undefined, token: CancellationToken) => ProviderResult export type ProvideWorkspaceDiagnosticSignature = (this: void, resultIds: PreviousResultId[], token: CancellationToken, resultReporter: ResultReporter) => ProviderResult export interface DiagnosticProviderMiddleware { handleDiagnostics?: (this: void, uri: string, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => void provideDiagnostics?: (this: void, document: TextDocument | URI, previousResultId: string | undefined, token: CancellationToken, next: ProvideDiagnosticSignature) => ProviderResult provideWorkspaceDiagnostics?: (this: void, resultIds: PreviousResultId[], token: CancellationToken, resultReporter: ResultReporter, next: ProvideWorkspaceDiagnosticSignature) => ProviderResult } export interface DiagnosticProviderShape { /** * An event that signals that the diagnostics should be refreshed for * all documents. */ onDidChangeDiagnosticsEmitter: Emitter /** * The provider of diagnostics. */ diagnostics: DiagnosticProvider /** * Forget the given document and remove all diagnostics. * @param document The document to forget. */ forget(document: TextDocument): void knows: (kind: PullState, textDocument: TextDocument) => boolean } export enum DiagnosticPullMode { onType = 'onType', onSave = 'onSave', onFocus = 'onFocus' } export interface DiagnosticPullOptions { /** * Whether to pull for diagnostics on document change. */ onChange?: boolean /** * Whether to pull for diagnostics on document save. */ onSave?: boolean /** * Whether to pull for diagnostics on editor focus. */ onFocus?: boolean /** * Whether to pull workspace diagnostics. */ workspace?: boolean /** * Minimatch patterns to match full filepath that should be ignored for pullDiagnostic. */ ignored?: string[] /** * An optional filter method that is consulted when triggering a diagnostic pull during document change or document * save or editor focus. * The document gets filtered if the method returns `true`. * @param document the document that changes or got save * @param mode the mode */ filter?(document: TextDocumentMatch, mode: DiagnosticPullMode): boolean /** * An optional match method that is consulted when pulling for diagnostics * when only a URI is known (e.g. for not instantiated tabs) * * The method should return `true` if the document selector matches the * given resource. See also the `vscode.languages.match` function. * @param documentSelector The document selector. * @param resource The resource. * @returns whether the resource is matched by the given document selector. */ match?(documentSelector: DocumentSelector, resource: URI): boolean } export interface $DiagnosticPullOptions { diagnosticPullOptions?: DiagnosticPullOptions } enum RequestStateKind { active = 'open', reschedule = 'reschedule', outDated = 'drop' } type RequestState = { state: RequestStateKind.active document: TextDocument | URI version: number | undefined tokenSource: CancellationTokenSource } | { state: RequestStateKind.reschedule document: TextDocument | URI } | { state: RequestStateKind.outDated document: TextDocument | URI } interface DocumentPullState { document: URI pulledVersion: number | undefined resultId: string | undefined } export enum PullState { document = 1, workspace = 2 } namespace DocumentOrUri { export function asKey(document: TextDocument | URI): string { return document instanceof URI ? document.toString() : document.uri } } const workspacePullDebounce = getConditionValue(3000, 10) export class DocumentPullStateTracker { private readonly documentPullStates: Map private readonly workspacePullStates: Map constructor() { this.documentPullStates = new Map() this.workspacePullStates = new Map() } public track(kind: PullState, textDocument: TextDocument): DocumentPullState public track(kind: PullState, uri: URI, version: number | undefined): DocumentPullState public track(kind: PullState, document: TextDocument | URI, arg1?: number): DocumentPullState { const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates const [key, uri, version] = document instanceof URI ? [document.toString(), document, arg1 as number | undefined] : [document.uri.toString(), URI.parse(document.uri), document.version] let state = states.get(key) if (state === undefined) { state = { document: uri, pulledVersion: version, resultId: undefined } states.set(key, state) } return state } public update(kind: PullState, textDocument: TextDocument, resultId: string | undefined): void public update(kind: PullState, uri: URI, version: number | undefined, resultId: string | undefined): void public update(kind: PullState, document: TextDocument | URI, arg1: string | number | undefined, arg2?: string): void { const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates const [key, uri, version, resultId] = document instanceof URI ? [document.toString(), document, arg1 as number | undefined, arg2] : [document.uri, URI.parse(document.uri), document.version, arg1 as string | undefined] let state = states.get(key) if (state === undefined) { state = { document: uri, pulledVersion: version, resultId } states.set(key, state) } else { state.pulledVersion = version state.resultId = resultId } } public unTrack(kind: PullState, document: TextDocument | URI): void { const key = DocumentOrUri.asKey(document) const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates states.delete(key) } public tracks(kind: PullState, document: TextDocument | URI): boolean { const key = DocumentOrUri.asKey(document) const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates return states.has(key) } public trackingDocuments(): string[] { return Array.from(this.documentPullStates.keys()) } public tracksSameVersion(kind: PullState, document: TextDocument): boolean { const key = document.uri.toString() const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates const state = states.get(key) return state !== undefined && state.pulledVersion === document.version } public getResultId(kind: PullState, document: TextDocument | URI): string | undefined { const key = DocumentOrUri.asKey(document) const states = kind === PullState.document ? this.documentPullStates : this.workspacePullStates return states.get(key)?.resultId } public getAllResultIds(): PreviousResultId[] { const result: PreviousResultId[] = [] for (let [uri, value] of this.workspacePullStates) { if (this.documentPullStates.has(uri)) { value = this.documentPullStates.get(uri)! } if (value.resultId !== undefined) { result.push({ uri, value: value.resultId }) } } return result } } export class DiagnosticRequestor extends BaseFeature implements Disposable { private isDisposed: boolean private enableWorkspace: boolean private readonly client: FeatureClient private readonly options: DiagnosticRegistrationOptions public readonly onDidChangeDiagnosticsEmitter: Emitter public readonly provider: DiagnosticProvider private readonly diagnostics: DiagnosticCollection private readonly openRequests: Map private readonly documentStates: DocumentPullStateTracker private workspaceErrorCounter: number private workspaceCancellation: CancellationTokenSource | undefined private workspaceTimeout: Disposable | undefined public constructor(client: FeatureClient, options: DiagnosticRegistrationOptions) { super(client) this.client = client this.options = options this.enableWorkspace = options.workspaceDiagnostics && this.client.clientOptions.diagnosticPullOptions?.workspace !== false this.isDisposed = false this.onDidChangeDiagnosticsEmitter = new Emitter() this.provider = this.createProvider() this.diagnostics = languages.createDiagnosticCollection(defaultValue(options.identifier, client.id)) this.openRequests = new Map() this.documentStates = new DocumentPullStateTracker() this.workspaceErrorCounter = 0 } public knows(kind: PullState, document: TextDocument | URI): boolean { return this.documentStates.tracks(kind, document) || this.openRequests.has(DocumentOrUri.asKey(document)) } public knowsSameVersion(kind: PullState, document: TextDocument): boolean { const requestState = this.openRequests.get(document.uri.toString()) if (requestState === undefined) { return this.documentStates.tracksSameVersion(kind, document) } // A reschedule request is independent of a version so it will trigger // on the latest version no matter what. if (requestState.state === RequestStateKind.reschedule) { return true } if (requestState.state === RequestStateKind.outDated) { return false } return requestState.version === document.version } public forget(kind: PullState, document: TextDocument | URI): void { this.documentStates.unTrack(kind, document) } public pull(document: TextDocument | URI, cb?: () => void): void { if (this.isDisposed) { return } const uri = DocumentOrUri.asKey(document) this.pullAsync(document).then(() => { if (cb) { cb() } }, error => { this.client.error(`Document pull failed for text document ${uri.toString()}`, error, false) }) } public async pullAsync(document: TextDocument | URI, version?: number): Promise { if (this.isDisposed) { return } const isUri = document instanceof URI const key = DocumentOrUri.asKey(document) version = isUri ? version : document.version const currentRequestState = this.openRequests.get(key) const documentState = isUri ? this.documentStates.track(PullState.document, document, version) : this.documentStates.track(PullState.document, document) if (currentRequestState === undefined) { const tokenSource = new CancellationTokenSource() this.openRequests.set(key, { state: RequestStateKind.active, document, version, tokenSource }) let report: DocumentDiagnosticReport | undefined let afterState: RequestState | undefined try { report = await this.provider.provideDiagnostics(document, documentState.resultId, tokenSource.token) ?? { kind: DocumentDiagnosticReportKind.Full, items: [] } } catch (error) { if (error instanceof LSPCancellationError && DiagnosticServerCancellationData.is(error.data) && error.data.retriggerRequest === false) { afterState = { state: RequestStateKind.outDated, document } } if (afterState === undefined && error instanceof CancellationError) { afterState = { state: RequestStateKind.reschedule, document } } else { throw error } } afterState = afterState ?? this.openRequests.get(key) if (afterState === undefined) { // This shouldn't happen. Log it this.client.error(`Lost request state in diagnostic pull model. Clearing diagnostics for ${key}`) this.diagnostics.delete(key) return } this.openRequests.delete(key) if (!workspace.tabs.isVisible(document)) { this.documentStates.unTrack(PullState.document, document) return } if (afterState.state === RequestStateKind.outDated) { return } // report is only undefined if the request has thrown. if (report !== undefined) { if (report.kind === DocumentDiagnosticReportKind.Full) { this.diagnostics.set(key, report.items) } documentState.pulledVersion = version documentState.resultId = report.resultId } if (afterState.state === RequestStateKind.reschedule) { this.pull(document) } } else { if (currentRequestState.state === RequestStateKind.active) { // Cancel the current request and reschedule a new one when the old one returned. currentRequestState.tokenSource.cancel() this.openRequests.set(key, { state: RequestStateKind.reschedule, document: currentRequestState.document }) } else if (currentRequestState.state === RequestStateKind.outDated) { this.openRequests.set(key, { state: RequestStateKind.reschedule, document: currentRequestState.document }) } } } public forgetDocument(document: TextDocument | URI): void { const key = DocumentOrUri.asKey(document) const request = this.openRequests.get(key) if (this.options.workspaceDiagnostics) { // If we run workspace diagnostic pull a last time for the diagnostics // and the rely on getting them from the workspace result. if (request !== undefined) { this.openRequests.set(key, { state: RequestStateKind.reschedule, document }) } else { this.pull(document, () => { this.forget(PullState.document, document) }) } // The previous resultId from the workspace pull state can map to diagnostics we no longer have // (e.g. they came from a workspace report but were overwritten by a later document pull request). // Clear the workspace pull state for this document as well to ensure we get fresh diagnostics. this.forget(PullState.workspace, document) } else { // We have normal pull or inter file dependencies. In this case we // clear the diagnostics (to have the same start as after startup). // We also cancel outstanding requests. if (request !== undefined) { if (request.state === RequestStateKind.active) { request.tokenSource.cancel() } this.openRequests.set(key, { state: RequestStateKind.outDated, document }) } this.diagnostics.delete(key) this.forget(PullState.document, document) } } public pullWorkspace(): void { if (!this.enableWorkspace) return this.pullWorkspaceAsync().then(() => { this.workspaceTimeout = RAL().timer.setTimeout(() => { this.pullWorkspace() }, workspacePullDebounce) }, error => { if (!(error instanceof LSPCancellationError) && !DiagnosticServerCancellationData.is(error.data)) { this.client.error(`Workspace diagnostic pull failed.`, error) this.workspaceErrorCounter++ } if (this.workspaceErrorCounter <= 5) { this.workspaceTimeout = RAL().timer.setTimeout(() => { this.pullWorkspace() }, workspacePullDebounce) } }) } private async pullWorkspaceAsync(): Promise { if (!this.provider.provideWorkspaceDiagnostics) { return } if (this.workspaceCancellation !== undefined) { this.workspaceCancellation.cancel() this.workspaceCancellation = undefined } this.workspaceCancellation = new CancellationTokenSource() const previousResultIds: PreviousResultId[] = this.documentStates.getAllResultIds() await this.provider.provideWorkspaceDiagnostics(previousResultIds, this.workspaceCancellation.token, chunk => { if (!chunk || this.isDisposed) { return } for (const item of chunk.items) { if (item.kind === DocumentDiagnosticReportKind.Full) { // Favour document pull result over workspace results. So skip if it is tracked // as a document result. if (!this.documentStates.tracks(PullState.document, URI.parse(item.uri))) { this.diagnostics.set(item.uri.toString(), item.items) } } this.documentStates.update(PullState.workspace, URI.parse(item.uri), item.version ?? undefined, item.resultId) } }) } private createProvider(): DiagnosticProvider { const provider: DiagnosticProvider = { onDidChangeDiagnostics: this.onDidChangeDiagnosticsEmitter.event, provideDiagnostics: (document, previousResultId, token) => { const middleware = this.client.middleware! const client = this._client const provideDiagnostics: ProvideDiagnosticSignature = (document, previousResultId, token) => { const uri = client.code2ProtocolConverter.asUri(document instanceof URI ? document : URI.parse(document.uri)) const params: DocumentDiagnosticParams = { identifier: this.options.identifier, textDocument: { uri }, previousResultId } return this.sendRequest(DocumentDiagnosticRequest.type, params, token, { kind: DocumentDiagnosticReportKind.Full, items: [] }).then(async result => { if (result === undefined || result === null || this.isDisposed) { return { kind: DocumentDiagnosticReportKind.Full, items: [] } } // make handleDiagnostics middleware works if (middleware.handleDiagnostics && result.kind == DocumentDiagnosticReportKind.Full) { middleware.handleDiagnostics(uri, result.items, (_, diagnostics) => { result.items = diagnostics }) } return result }) } return middleware.provideDiagnostics ? middleware.provideDiagnostics(document, previousResultId, token, provideDiagnostics) : provideDiagnostics(document, previousResultId, token) } } if (this.options.workspaceDiagnostics) { provider.provideWorkspaceDiagnostics = (resultIds, token, resultReporter): ProviderResult => { const provideWorkspaceDiagnostics: ProvideWorkspaceDiagnosticSignature = (resultIds, token, resultReporter): ProviderResult => { const partialResultToken = uuid() const disposable = this.client.onProgress(WorkspaceDiagnosticRequest.partialResult, partialResultToken, partialResult => { if (partialResult == undefined) { resultReporter(null) return } resultReporter(partialResult as WorkspaceDiagnosticReportPartialResult) }) const params: WorkspaceDiagnosticParams & { __token?: string } = { identifier: this.options.identifier, previousResultIds: resultIds, partialResultToken } return this.sendRequest(WorkspaceDiagnosticRequest.type, params, token, { items: [] }).then(async (result): Promise => { resultReporter(result) return { items: [] } }).finally(() => { disposable.dispose() }) } const middleware: DiagnosticProviderMiddleware = this.client.middleware! return middleware.provideWorkspaceDiagnostics ? middleware.provideWorkspaceDiagnostics(resultIds, token, resultReporter, provideWorkspaceDiagnostics) : provideWorkspaceDiagnostics(resultIds, token, resultReporter) } } return provider } public dispose(): void { this.isDisposed = true // Cancel and clear workspace pull if present. this.workspaceCancellation?.cancel() this.workspaceTimeout?.dispose() // Cancel all request and mark open requests as outdated. for (const request of this.openRequests.values()) { if (request.state === RequestStateKind.active) { request.tokenSource.cancel() } } this.openRequests.clear() } } const timeoutDebounce = getConditionValue(500, 10) export class BackgroundScheduler implements Disposable { private readonly client: FeatureClient private readonly diagnosticRequestor: DiagnosticRequestor private lastDocumentToPull: TextDocument | URI | undefined private readonly documents: LinkedMap private timeoutHandle: Disposable | undefined // The problem is that there could be outstanding diagnostic requests // when we shutdown which when we receive the result will trigger a // reschedule. So we remember if the background scheduler got disposed // and ignore those re-schedules private isDisposed: boolean public constructor(client: FeatureClient, diagnosticRequestor: DiagnosticRequestor) { this.client = client this.diagnosticRequestor = diagnosticRequestor this.documents = new LinkedMap() this.isDisposed = false } public add(document: TextDocument): void { if (this.isDisposed === true) { return } const key = document.uri if (this.documents.has(key)) { return } this.documents.set(key, document, Touch.AsNew) // Make sure we run up to that document. We could // consider inserting it after the current last // document for performance reasons but it might not catch // all interfile dependencies. this.lastDocumentToPull = document } public remove(document: TextDocument | URI): void { const key = DocumentOrUri.asKey(document) this.documents.delete(key) // No more documents. Stop background activity. if (this.documents.size === 0) { this.stop() return } if (key === this.lastDocumentToPullKey()) { // The remove document was the one we would run up to. So // take the one before it. const before = this.documents.before(key) if (before === undefined) { this.stop() } else { this.lastDocumentToPull = before } } } public trigger(): void { this.lastDocumentToPull = this.documents.last this.runLoop() } private runLoop(): void { if (this.isDisposed === true) { return } // We have an empty background list. Make sure we stop // background activity. if (this.documents.size === 0) { this.stop() return } // We have no last document anymore so stop the loop if (this.lastDocumentToPull === undefined) { return } // We have a timeout in the loop. So we should not schedule // another run. if (this.timeoutHandle !== undefined) { return } this.timeoutHandle = RAL().timer.setTimeout(() => { const document = this.documents.first if (document === undefined) { return } const key = DocumentOrUri.asKey(document) this.diagnosticRequestor.pullAsync(document).catch(error => { this.client.error(`Document pull failed for text document ${key}`, error, false) }).finally(() => { this.timeoutHandle = undefined this.documents.set(key, document, Touch.Last) if (key !== this.lastDocumentToPullKey()) { this.runLoop() } }) }, timeoutDebounce) } public dispose(): void { this.stop() this.documents.clear() this.lastDocumentToPull = undefined } private stop(): void { this.timeoutHandle?.dispose() this.timeoutHandle = undefined this.lastDocumentToPull = undefined } private lastDocumentToPullKey(): string | undefined { return this.lastDocumentToPull !== undefined ? DocumentOrUri.asKey(this.lastDocumentToPull) : undefined } } class DiagnosticFeatureProviderImpl implements DiagnosticProviderShape { public readonly disposable: Disposable private readonly diagnosticRequestor: DiagnosticRequestor private activeTextDocument: TextDocument | undefined private readonly backgroundScheduler: BackgroundScheduler constructor(client: FeatureClient, options: DiagnosticRegistrationOptions) { const diagnosticPullOptions = Object.assign({ onChange: false, onSave: false, onFocus: false }, client.clientOptions.diagnosticPullOptions) const selector = options.documentSelector ?? [] const disposables: Disposable[] = [] const ignored = diagnosticPullOptions.ignored ?? [] const matches = (document: TextDocument): boolean => { if (diagnosticPullOptions.match !== undefined) { return diagnosticPullOptions.match(selector, URI.parse(document.uri)) } if (workspace.match(selector, document) <= 0 || !workspace.tabs.isVisible(document)) return false if (ignored.length > 0 && ignored.some(p => minimatch(URI.parse(document.uri).fsPath, p, { dot: true }))) return false return true } const isActiveDocument = (document: TextDocument): boolean => { return document.uri === this.activeTextDocument?.uri } const considerDocument = (textDocument: TextDocument, mode: DiagnosticPullMode): boolean => { return (diagnosticPullOptions.filter === undefined || !diagnosticPullOptions.filter(textDocument, mode)) && this.diagnosticRequestor.knows(PullState.document, textDocument) } this.diagnosticRequestor = new DiagnosticRequestor(client, options) this.backgroundScheduler = new BackgroundScheduler(client, this.diagnosticRequestor) const addToBackgroundIfNeeded = (document: TextDocument): void => { if (!matches(document) || !options.interFileDependencies || isActiveDocument(document) || diagnosticPullOptions.onChange === false) return this.backgroundScheduler.add(document) } this.activeTextDocument = window.activeTextEditor?.document.textDocument disposables.push(window.onDidChangeActiveTextEditor(editor => { const oldActive = this.activeTextDocument this.activeTextDocument = editor?.document.textDocument if (oldActive !== undefined) { addToBackgroundIfNeeded(oldActive) } if (this.activeTextDocument !== undefined) { this.backgroundScheduler.remove(this.activeTextDocument) if (diagnosticPullOptions.onFocus === true && matches(this.activeTextDocument) && considerDocument(this.activeTextDocument, DiagnosticPullMode.onFocus)) { this.diagnosticRequestor.pull(this.activeTextDocument) } } })) // We always pull on open. const openFeature = client.getFeature(DidOpenTextDocumentNotification.method) disposables.push(openFeature.onNotificationSent(event => { const textDocument = event.original if (this.diagnosticRequestor.knowsSameVersion(PullState.document, textDocument)) { return } if (matches(textDocument)) { this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument) }) } })) disposables.push(workspace.tabs.onOpen(opened => { for (const resource of opened) { // We already know about this document. This can happen via a document open. if (this.diagnosticRequestor.knows(PullState.document, resource)) { continue } const uriStr = resource.toString() let textDocument = workspace.getDocument(uriStr)!.textDocument if (textDocument !== undefined && matches(textDocument)) { this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument!) }) } } })) // Pull all diagnostics for documents that are already open for (const textDocument of workspace.textDocuments) { if (matches(textDocument)) { this.diagnosticRequestor.pull(textDocument, () => { addToBackgroundIfNeeded(textDocument) }) } } if (diagnosticPullOptions.onChange === true) { const changeFeature = client.getFeature(DidChangeTextDocumentNotification.method) disposables.push(changeFeature.onNotificationSent(async event => { const textDocument = workspace.getDocument(event.original.bufnr).textDocument if (considerDocument(textDocument, DiagnosticPullMode.onType)) { this.diagnosticRequestor.pull(textDocument, () => { this.backgroundScheduler.trigger() }) } })) } if (diagnosticPullOptions.onSave === true) { const saveFeature = client.getFeature(DidSaveTextDocumentNotification.method) disposables.push(saveFeature.onNotificationSent(event => { const textDocument = event.original if (considerDocument(textDocument, DiagnosticPullMode.onSave)) { this.diagnosticRequestor.pull(event.original) } })) } const closeFeature = client.getFeature(DidCloseTextDocumentNotification.method) disposables.push(closeFeature.onNotificationSent(event => { this.cleanUpDocument(event.original) })) // Same when a tabs closes. disposables.push(workspace.tabs.onClose(closed => { for (const document of closed) { this.cleanUpDocument(document) } })) // We received a did change from the server. this.diagnosticRequestor.onDidChangeDiagnosticsEmitter.event(() => { for (const textDocument of workspace.textDocuments) { if (matches(textDocument)) { this.diagnosticRequestor.pull(textDocument) } } }) // da348dc5-c30a-4515-9d98-31ff3be38d14 is the test UUID to test the middle ware. So don't auto trigger pulls. if (options.workspaceDiagnostics === true && options.identifier !== 'da348dc5-c30a-4515-9d98-31ff3be38d14') { this.diagnosticRequestor.pullWorkspace() } this.disposable = Disposable.create(() => [...disposables, this.backgroundScheduler, this.diagnosticRequestor].forEach(d => d.dispose())) } public get onDidChangeDiagnosticsEmitter(): Emitter { return this.diagnosticRequestor.onDidChangeDiagnosticsEmitter } public get diagnostics(): DiagnosticProvider { return this.diagnosticRequestor.provider } public knows(kind: PullState, textDocument: TextDocument): boolean { return this.diagnosticRequestor.knows(kind, textDocument) } public forget(document: TextDocument): void { this.cleanUpDocument(document) } private cleanUpDocument(document: TextDocument | URI): void { this.backgroundScheduler.remove(document) if (this.diagnosticRequestor.knows(PullState.document, document)) { this.diagnosticRequestor.forgetDocument(document) } } } export interface DiagnosticFeatureShape { refresh(): void } export class DiagnosticFeature extends TextDocumentLanguageFeature implements DiagnosticFeatureShape { constructor(client: FeatureClient) { super(client, DocumentDiagnosticRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let capability = ensure(ensure(capabilities, 'textDocument')!, 'diagnostic')! capability.relatedInformation = true capability.tagSupport = { valueSet: [DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated] } capability.codeDescriptionSupport = true capability.dataSupport = true capability.dynamicRegistration = true capability.relatedDocumentSupport = true ensure(ensure(capabilities, 'workspace')!, 'diagnostics')!.refreshSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const client = this._client client.onRequest(DiagnosticRefreshRequest.type, async () => { for (const provider of this.getAllProviders()) { provider.onDidChangeDiagnosticsEmitter.fire() } }) let [id, options] = this.getRegistration(documentSelector, capabilities.diagnosticProvider) if (!id || !options) return this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: DiagnosticRegistrationOptions): [Disposable, DiagnosticProviderShape] { const provider = new DiagnosticFeatureProviderImpl(this._client, options) return [provider.disposable, provider] } public refresh(): void { for (const provider of this.getAllProviders()) { provider.onDidChangeDiagnosticsEmitter.fire() } } } ================================================ FILE: src/language-client/documentHighlight.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentHighlight, DocumentHighlightOptions, DocumentHighlightRegistrationOptions, DocumentSelector, Position, ServerCapabilities, TextDocumentRegistrationOptions } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { DocumentHighlightProvider, ProviderResult } from '../provider' import { DocumentHighlightRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideDocumentHighlightsSignature { ( this: void, document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } export interface DocumentHighlightMiddleware { provideDocumentHighlights?: ( this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideDocumentHighlightsSignature ) => ProviderResult } export class DocumentHighlightFeature extends TextDocumentLanguageFeature< boolean | DocumentHighlightOptions, DocumentHighlightRegistrationOptions, DocumentHighlightProvider, DocumentHighlightMiddleware > { constructor(client: FeatureClient) { super(client, DocumentHighlightRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure( ensure(capabilities, 'textDocument')!, 'documentHighlight' )!.dynamicRegistration = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.documentHighlightProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: TextDocumentRegistrationOptions ): [Disposable, DocumentHighlightProvider] { const provider: DocumentHighlightProvider = { provideDocumentHighlights: (document, position, token) => { const client = this._client const _provideDocumentHighlights: ProvideDocumentHighlightsSignature = (document, position, token) => { return this.sendRequest( DocumentHighlightRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token ) } const middleware = client.middleware! return middleware.provideDocumentHighlights ? middleware.provideDocumentHighlights(document, position, token, _provideDocumentHighlights) : _provideDocumentHighlights(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerDocumentHighlightProvider(options.documentSelector!, provider), provider] } } ================================================ FILE: src/language-client/documentLink.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentLink, DocumentLinkOptions, DocumentLinkRegistrationOptions, DocumentSelector, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { DocumentLinkProvider, ProviderResult } from '../provider' import { DocumentLinkRequest, DocumentLinkResolveRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideDocumentLinksSignature { (this: void, document: TextDocument, token: CancellationToken): ProviderResult } export interface ResolveDocumentLinkSignature { (this: void, link: DocumentLink, token: CancellationToken): ProviderResult } export interface DocumentLinkMiddleware { provideDocumentLinks?: ( this: void, document: TextDocument, token: CancellationToken, next: ProvideDocumentLinksSignature ) => ProviderResult resolveDocumentLink?: ( this: void, link: DocumentLink, token: CancellationToken, next: ResolveDocumentLinkSignature ) => ProviderResult } export class DocumentLinkFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, DocumentLinkRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const documentLinkCapabilities = ensure(ensure(capabilities, 'textDocument')!, 'documentLink')! documentLinkCapabilities.dynamicRegistration = true documentLinkCapabilities.tooltipSupport = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.documentLinkProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: DocumentLinkRegistrationOptions ): [Disposable, DocumentLinkProvider] { const provider: DocumentLinkProvider = { provideDocumentLinks: (document: TextDocument, token: CancellationToken): ProviderResult => { const client = this._client const provideDocumentLinks: ProvideDocumentLinksSignature = (document, token) => { return this.sendRequest( DocumentLinkRequest.type, client.code2ProtocolConverter.asDocumentLinkParams(document), token ) } const middleware = client.middleware! return middleware.provideDocumentLinks ? middleware.provideDocumentLinks(document, token, provideDocumentLinks) : provideDocumentLinks(document, token) }, resolveDocumentLink: options.resolveProvider ? (link, token) => { const client = this._client let resolveDocumentLink: ResolveDocumentLinkSignature = (link, token) => { return this.sendRequest(DocumentLinkResolveRequest.type, link, token, link) } const middleware = client.middleware! return middleware.resolveDocumentLink ? middleware.resolveDocumentLink(link, token, resolveDocumentLink) : resolveDocumentLink(link, token) } : undefined } this._client.attachExtensionName(provider) return [languages.registerDocumentLinkProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/documentSymbol.ts ================================================ 'use strict' import type { ClientCapabilities, DocumentSelector, DocumentSymbolOptions, DocumentSymbolRegistrationOptions, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import { DocumentSymbol, SymbolInformation, SymbolKind, SymbolTag } from 'vscode-languageserver-types' import languages from '../languages' import { DocumentSymbolProvider, ProviderResult } from '../provider' import { CancellationToken, Disposable, DocumentSymbolRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export const SupportedSymbolKinds: SymbolKind[] = [ SymbolKind.File, SymbolKind.Module, SymbolKind.Namespace, SymbolKind.Package, SymbolKind.Class, SymbolKind.Method, SymbolKind.Property, SymbolKind.Field, SymbolKind.Constructor, SymbolKind.Enum, SymbolKind.Interface, SymbolKind.Function, SymbolKind.Variable, SymbolKind.Constant, SymbolKind.String, SymbolKind.Number, SymbolKind.Boolean, SymbolKind.Array, SymbolKind.Object, SymbolKind.Key, SymbolKind.Null, SymbolKind.EnumMember, SymbolKind.Struct, SymbolKind.Event, SymbolKind.Operator, SymbolKind.TypeParameter ] export const SupportedSymbolTags: SymbolTag[] = [ SymbolTag.Deprecated ] export interface ProvideDocumentSymbolsSignature { (this: void, document: TextDocument, token: CancellationToken): ProviderResult } export interface DocumentSymbolMiddleware { provideDocumentSymbols?: ( this: void, document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature ) => ProviderResult } export class DocumentSymbolFeature extends TextDocumentLanguageFeature< boolean | DocumentSymbolOptions, DocumentSymbolRegistrationOptions, DocumentSymbolProvider, DocumentSymbolMiddleware > { constructor(client: FeatureClient) { super(client, DocumentSymbolRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let symbolCapabilities = ensure(ensure(capabilities, 'textDocument')!, 'documentSymbol')! as any symbolCapabilities.dynamicRegistration = true symbolCapabilities.symbolKind = { valueSet: SupportedSymbolKinds } symbolCapabilities.hierarchicalDocumentSymbolSupport = true symbolCapabilities.tagSupport = { valueSet: SupportedSymbolTags } symbolCapabilities.labelSupport = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.documentSymbolProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: DocumentSymbolRegistrationOptions ): [Disposable, DocumentSymbolProvider] { const provider: DocumentSymbolProvider = { meta: options.label ? { label: options.label } : undefined, provideDocumentSymbols: (document, token) => { const client = this._client const _provideDocumentSymbols: ProvideDocumentSymbolsSignature = (document, token) => { return this.sendRequest( DocumentSymbolRequest.type, client.code2ProtocolConverter.asDocumentSymbolParams(document), token ) } const middleware = client.middleware! return middleware.provideDocumentSymbols ? middleware.provideDocumentSymbols(document, token, _provideDocumentSymbols) : _provideDocumentSymbols(document, token) } } this._client.attachExtensionName(provider) return [languages.registerDocumentSymbolProvider(options.documentSelector!, provider), provider] } } ================================================ FILE: src/language-client/executeCommand.ts ================================================ 'use strict' import type { ClientCapabilities, Disposable, ExecuteCommandParams, ExecuteCommandRegistrationOptions, RegistrationType, ServerCapabilities } from 'vscode-languageserver-protocol' import commands from '../commands' import { ProviderResult } from '../provider' import { CancellationToken, ExecuteCommandRequest } from '../util/protocol' import { BaseFeature, DynamicFeature, ensure, FeatureClient, FeatureState, RegistrationData } from './features' import * as UUID from './utils/uuid' export interface ExecuteCommandSignature { (this: void, command: string, args: any[]): ProviderResult } export interface ExecuteCommandMiddleware { executeCommand?: (this: void, command: string, args: any[], next: ExecuteCommandSignature) => ProviderResult } export class ExecuteCommandFeature extends BaseFeature implements DynamicFeature { private _commands: Map = new Map() constructor(client: FeatureClient) { super(client) } public getState(): FeatureState { return { kind: 'workspace', id: this.registrationType.method, registrations: this._commands.size > 0 } } public get registrationType(): RegistrationType { return ExecuteCommandRequest.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'workspace')!, 'executeCommand')!.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities): void { if (!capabilities.executeCommandProvider) { return } this.register({ id: UUID.generateUuid(), registerOptions: Object.assign({}, capabilities.executeCommandProvider) }) } public register( data: RegistrationData ): void { const client = this._client const middleware = client.middleware! const executeCommand: ExecuteCommandSignature = (command: string, args: any[]): any => { const params: ExecuteCommandParams = { command, arguments: args } return client.sendRequest(ExecuteCommandRequest.type, params, CancellationToken.None).then( undefined, error => { client.handleFailedRequest(ExecuteCommandRequest.type, undefined, error, undefined) } ) } if (data.registerOptions.commands) { let disposables: Disposable[] = [] for (const command of data.registerOptions.commands) { disposables.push(commands.registerCommand(command, (...args: any[]) => { return middleware.executeCommand ? middleware.executeCommand(command, args, executeCommand) : executeCommand(command, args) }, null, true)) } this._commands.set(data.id, disposables) } } public unregister(id: string): void { let disposables = this._commands.get(id) if (disposables) { this._commands.delete(id) disposables.forEach(disposable => disposable.dispose()) } } public dispose(): void { this._commands.forEach(value => { value.forEach(disposable => disposable.dispose()) }) this._commands.clear() } } ================================================ FILE: src/language-client/features.ts ================================================ 'use strict' import type { CallHierarchyPrepareRequest, CancellationToken, ClientCapabilities, CodeActionRequest, CodeLensRequest, CompletionRequest, DeclarationRequest, DefinitionRequest, DidChangeTextDocumentNotification, DidChangeWatchedFilesNotification, DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersNotification, DidCloseTextDocumentNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidOpenTextDocumentNotification, DidRenameFilesNotification, DidSaveTextDocumentNotification, Disposable, DocumentColorRequest, DocumentDiagnosticRequest, DocumentFormattingRequest, DocumentHighlightRequest, DocumentLinkRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest, DocumentSelector, DocumentSymbolRequest, ExecuteCommandRegistrationOptions, ExecuteCommandRequest, FileOperationRegistrationOptions, FoldingRangeRequest, GenericNotificationHandler, GenericRequestHandler, HoverRequest, ImplementationRequest, InitializeParams, InitializeResult, InlayHintRequest, InlineCompletionRequest, InlineValueRequest, LinkedEditingRangeRequest, MarkupKind, MessageSignature, NotificationHandler, NotificationHandler0, NotificationType, NotificationType0, ProgressType, ProtocolNotificationType, ProtocolNotificationType0, ProtocolRequestType, ProtocolRequestType0, ReferencesRequest, RegistrationType, RenameRequest, RequestHandler, RequestHandler0, RequestParam, RequestType, RequestType0, SelectionRangeRequest, SemanticTokensRegistrationType, ServerCapabilities, SignatureHelpRequest, TextEdit, Trace, TraceOptions, Tracer, TypeDefinitionRequest, TypeHierarchyPrepareRequest, WillCreateFilesRequest, WillDeleteFilesRequest, WillRenameFilesRequest, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { FileCreateEvent, FileDeleteEvent, FileRenameEvent, FileWillCreateEvent, FileWillDeleteEvent, FileWillRenameEvent, TextDocumentWillSaveEvent } from '../core/files' import { CallHierarchyProvider, CodeActionProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentLinkProvider, DocumentRangeFormattingEditProvider, DocumentSymbolProvider, HoverProvider, ImplementationProvider, InlineCompletionItemProvider, LinkedEditingRangeProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, TypeHierarchyProvider, WorkspaceSymbolProvider } from '../provider' import { CancellationError } from '../util/errors' import * as Is from '../util/is' import { Emitter, Event, StaticRegistrationOptions, TextDocumentRegistrationOptions, WorkDoneProgressOptions } from '../util/protocol' import workspace from '../workspace' import * as c2p from './utils/codeConverter' import * as UUID from './utils/uuid' export class LSPCancellationError extends CancellationError { public readonly data: object constructor(data: object) { super() this.data = data } } export interface Connection { id: string listen(): void hasPendingResponse(): boolean sendRequest(type: ProtocolRequestType0, token?: CancellationToken): Promise sendRequest(type: ProtocolRequestType, params: NoInfer>, token?: CancellationToken): Promise sendRequest(type: RequestType0, token?: CancellationToken): Promise sendRequest(type: RequestType, params: NoInfer>, token?: CancellationToken): Promise sendRequest(method: string, token?: CancellationToken): Promise sendRequest(method: string, param: any, token?: CancellationToken): Promise sendRequest(type: string | MessageSignature, ...params: any[]): Promise onRequest(type: ProtocolRequestType0, handler: NoInfer>): Disposable onRequest(type: ProtocolRequestType, handler: NoInfer>): Disposable onRequest(type: RequestType0, handler: NoInfer>): Disposable onRequest(type: RequestType, handler: NoInfer>): Disposable onRequest(method: string | MessageSignature, handler: GenericRequestHandler): Disposable sendNotification(type: ProtocolNotificationType0): Promise sendNotification(type: ProtocolNotificationType, params?: NoInfer>): Promise sendNotification(type: NotificationType0): Promise sendNotification

(type: NotificationType

, params?: NoInfer>): Promise sendNotification(method: string | MessageSignature, params?: any): Promise onNotification(type: ProtocolNotificationType0, handler: NotificationHandler0): Disposable onNotification(type: ProtocolNotificationType, handler: NoInfer>): Disposable onNotification(type: NotificationType0, handler: NotificationHandler0): Disposable onNotification

(type: NotificationType

, handler: NoInfer>): Disposable onNotification(method: string | MessageSignature, handler: GenericNotificationHandler): Disposable onProgress

(type: ProgressType

, token: string | number, handler: NoInfer>): Disposable sendProgress

(type: ProgressType

, token: string | number, value: P): Promise trace(value: Trace, tracer: Tracer, sendNotification?: boolean | TraceOptions): Promise initialize(params: InitializeParams): Promise shutdown(): Promise exit(): Promise end(): void dispose(): void } export class BaseFeature { protected readonly _client: FeatureClient constructor(client: FeatureClient) { this._client = client } protected sendRequest(type: RequestType, params: RequestParam

, token: CancellationToken, defaultValue?: R): Promise { return this._client.sendRequest(type, params, token).then((res => { return token.isCancellationRequested || res == null ? defaultValue ?? null : res }), error => { return this._client.handleFailedRequest(type, token, error, defaultValue ?? null) }) } } export interface RegistrationData { id: string registerOptions: T } export interface NextSignature { (this: void, data: P, next: (data: P) => R): R } export function ensure(target: T, key: K): T[K] { if (target[key] === undefined) { target[key] = {} as any } return target[key] } export interface TextDocumentProviderFeature { readonly registrationLength: number /** * Triggers the corresponding RPC method. */ getProvider(textDocument: TextDocument): T | undefined } export type FeatureStateKind = 'document' | 'workspace' | 'static' | 'window' export type FeatureState = { kind: 'document' /** * The features's id. This is usually the method names used during * registration. */ id: string /** * Has active registrations. */ registrations: boolean /** * A registration matches an open document. */ matches: boolean } | { kind: 'workspace' /** * The features's id. This is usually the method names used during * registration. */ id: string /** * Has active registrations. */ registrations: boolean } | { kind: 'window' /** * The features's id. This is usually the method names used during * registration. */ id: string /** * Has active registrations. */ registrations: boolean } | { kind: 'static' } interface TextDocumentFeatureRegistration { disposable: Disposable data: RegistrationData provider: PR } /** * A static feature. A static feature can't be dynamically activated via the * server. It is wired during the initialize sequence. */ export interface StaticFeature { readonly method: string /** * Called to fill the initialize params. * @params the initialize params. */ fillInitializeParams?: (params: InitializeParams) => void /** * Called to fill in the client capabilities this feature implements. * @param capabilities The client capabilities to fill. */ fillClientCapabilities(capabilities: ClientCapabilities): void /** * A preflight where the server capabilities are shown to all features * before a feature is actually initialized. This allows feature to * capture some state if they are a pre-requisite for other features. * @param capabilities the server capabilities * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ preInitialize?: (capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined) => void /** * Initialize the feature. This method is called on a feature instance * when the client has successfully received the initialize request from * the server and before the client sends the initialized notification * to the server. * @param capabilities the server capabilities * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined): void /** * Returns the state the feature is in. */ getState?(): FeatureState /** * Called when the client is stopped to dispose this feature. Usually a feature * un-registers listeners registered hooked up with the VS Code extension host. */ dispose(): void } // eslint-disable-next-line no-redeclare export namespace StaticFeature { export function is(value: any): value is StaticFeature { return value !== undefined && value !== null && Is.func(value.fillClientCapabilities) && Is.func(value.initialize) && Is.func(value.dispose) && (value.fillInitializeParams === undefined || Is.func(value.fillInitializeParams)) && value.registrationType === undefined } } /** * A dynamic feature can be activated via the server. */ export interface DynamicFeature { /** * Called to fill the initialize params. * @params the initialize params. */ fillInitializeParams?: (params: InitializeParams) => void /** * Called to fill in the client capabilities this feature implements. * @param capabilities The client capabilities to fill. */ fillClientCapabilities(capabilities: ClientCapabilities): void /** * A preflight where the server capabilities are shown to all features * before a feature is actually initialized. This allows feature to * capture some state if they are a pre-requisite for other features. * @param capabilities the server capabilities * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ preInitialize?: (capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined) => void /** * Initialize the feature. This method is called on a feature instance * when the client has successfully received the initialize request from * the server and before the client sends the initialized notification * to the server. * @param capabilities the server capabilities. * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined): void /** * Returns the state the feature is in. */ getState(): FeatureState /** * The signature (e.g. method) for which this features support dynamic activation / registration. */ registrationType: RegistrationType /** * Is called when the server send a register request for the given message. * @param data additional registration data as defined in the protocol. */ register(data: RegistrationData): void /** * Is called when the server wants to unregister a feature. * @param id the id used when registering the feature. */ unregister(id: string): void /** * Called when the client is stopped to dispose this feature. Usually a feature * un-registers listeners registered hooked up with the VS Code extension host. */ dispose(): void } // eslint-disable-next-line no-redeclare export namespace DynamicFeature { export function is(value: any): value is DynamicFeature { const candidate: DynamicFeature = value return candidate !== undefined && candidate !== null && Is.func(candidate.fillClientCapabilities) && Is.func(candidate.initialize) && Is.func(candidate.dispose) && (candidate.fillInitializeParams === undefined || Is.func(candidate.fillInitializeParams)) && Is.func(candidate.register) && Is.func(candidate.unregister) && candidate.registrationType !== undefined } } interface CreateParamsSignature { (data: E): RequestParam

} /** * An abstract dynamic feature implementation that operates on documents (e.g. text * documents or notebooks). */ export abstract class DynamicDocumentFeature extends BaseFeature implements DynamicFeature { constructor(client: FeatureClient) { super(client) } // Repeat from interface. public abstract fillClientCapabilities(capabilities: ClientCapabilities): void public abstract initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined): void public abstract registrationType: RegistrationType public abstract register(data: RegistrationData): void public abstract unregister(id: string): void public abstract dispose(): void /** * Returns the state the feature is in. */ public getState(): FeatureState { const selectors = this.getDocumentSelectors() let count = 0 for (const selector of selectors) { count++ for (const document of workspace.textDocuments) { if (workspace.match(selector, document) > 0) { return { kind: 'document', id: this.registrationType.method, registrations: true, matches: true } } } } const registrations = count > 0 return { kind: 'document', id: this.registrationType.method, registrations, matches: false } } protected abstract getDocumentSelectors(): IterableIterator public sendWithMiddleware(fn: (...args: any[]) => ProviderResult, key: string, ...params: any[]): ProviderResult { const middleware = this._client.middleware! return middleware[key] ? middleware[key](...params, fn) : fn(...params) } } /** * A mixin type that allows to send notification or requests using a registered * provider. */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export interface TextDocumentSendFeature { /** * Returns a provider for the given text document. */ getProvider(document: TextDocument): { send: T } | undefined } export interface NotificationSendEvent { original: E type: ProtocolNotificationType params: RequestParam

} export interface NotifyingFeature { onNotificationSent: Event> } export abstract class TextDocumentEventFeature extends DynamicDocumentFeature implements TextDocumentSendFeature<(data: E) => Promise>, NotifyingFeature { private readonly _event: Event protected readonly _type: ProtocolNotificationType protected readonly _middleware: string protected readonly _createParams: CreateParamsSignature protected readonly _selectorFilter?: (selectors: IterableIterator, data: E) => boolean private _listener: Disposable | undefined protected readonly _selectors: Map private _onNotificationSent: Emitter> public static textDocumentFilter( selectors: IterableIterator, textDocument: TextDocument ): boolean { for (const selector of selectors) { if (workspace.match(selector, textDocument) > 0) { return true } } return false } constructor(client: FeatureClient, event: Event, type: ProtocolNotificationType, middleware: string, createParams: NoInfer>, selectorFilter?: (selectors: IterableIterator, data: E) => boolean ) { super(client) this._event = event this._type = type this._middleware = middleware this._createParams = createParams this._selectorFilter = selectorFilter this._selectors = new Map() this._onNotificationSent = new Emitter>() } protected getDocumentSelectors(): IterableIterator { return this._selectors.values() } public register(data: RegistrationData): void { if (!data.registerOptions.documentSelector) { return } if (!this._listener) { this._listener = this._event(data => { this.callback(data).catch(error => { this._client.error(`Sending document notification ${this._type.method} failed.`, error) }) }) } this._selectors.set(data.id, data.registerOptions.documentSelector) } protected async callback(data: E): Promise { if (!this.matches(data)) return return await this.sendNotification(data) } protected async sendNotification(data: E): Promise { const doSend = async (data: E): Promise => { const params = this._createParams(data) await this._client.sendNotification(this._type, params) this.notificationSent(data, this._type, params) } return this.sendWithMiddleware(doSend, this._middleware, data) } protected matches(data: E): boolean { return !this._selectorFilter || this._selectorFilter(this._selectors.values(), data) } public get onNotificationSent(): Event> { return this._onNotificationSent.event } protected notificationSent(data: E, type: ProtocolNotificationType, params: RequestParam

): void { this._onNotificationSent.fire({ original: data, type, params }) } public unregister(id: string): void { this._selectors.delete(id) } public dispose(): void { this._selectors.clear() this._onNotificationSent.dispose() this._onNotificationSent = new Emitter>() if (this._listener) { this._listener.dispose() this._listener = undefined } } public getProvider(document: TextDocument): { send: (data: E) => Promise } | undefined { for (const selector of this.getDocumentSelectors()) { if (workspace.match(selector, document) > 0) { return { send: (data: E) => { return this.callback(data) } } } } return undefined } } /** * A abstract feature implementation that registers language providers * for text documents using a given document selector. */ export abstract class TextDocumentLanguageFeature extends DynamicDocumentFeature { private readonly _registrationType: RegistrationType private readonly _registrations: Map> constructor(client: FeatureClient, registrationType: RegistrationType) { super(client) this._registrationType = registrationType this._registrations = new Map() } protected *getDocumentSelectors(): IterableIterator { for (const registration of this._registrations.values()) { const selector = registration.data.registerOptions.documentSelector if (selector != null) { yield selector } } } public get registrationType(): RegistrationType { return this._registrationType } public get registrationLength(): number { return this._registrations.size } public abstract fillClientCapabilities(capabilities: ClientCapabilities): void public abstract initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void public register(data: RegistrationData): void { if (!data.registerOptions.documentSelector) { return } let registration = this.registerLanguageProvider(data.registerOptions, data.id) this._registrations.set(data.id, { disposable: registration[0], data, provider: registration[1] }) } protected abstract registerLanguageProvider(options: RO, id: string): [Disposable, PR] public unregister(id: string): void { let registration = this._registrations.get(id) if (registration !== undefined) { this._registrations.delete(id) registration.disposable.dispose() } } public dispose(): void { this._registrations.forEach(value => { value.disposable.dispose() }) this._registrations.clear() } public getRegistration(documentSelector: DocumentSelector, capability: undefined | PO & { id?: string } | (RO & StaticRegistrationOptions)): [string | undefined, (RO & { documentSelector: DocumentSelector }) | undefined] { if (!capability) { return [undefined, undefined] } else if (TextDocumentRegistrationOptions.is(capability)) { const id = StaticRegistrationOptions.hasId(capability) ? capability.id : UUID.generateUuid() const selector = defaultValue(capability.documentSelector, documentSelector) return [id, Object.assign({}, capability, { documentSelector: selector })] } else if ((Is.boolean(capability) && capability === true) || WorkDoneProgressOptions.is(capability)) { const options = capability === true ? { documentSelector } : Object.assign({}, capability, { documentSelector }) return [UUID.generateUuid(), options as RO & { documentSelector: DocumentSelector }] } return [undefined, undefined] } protected getRegistrationOptions(documentSelector: DocumentSelector | undefined, capability: undefined | PO): (RO & { documentSelector: DocumentSelector }) | undefined { if (!documentSelector || !capability) { return undefined } return (Is.boolean(capability) && capability === true ? { documentSelector } : Object.assign({}, capability, { documentSelector })) as RO & { documentSelector: DocumentSelector } } public getProvider(textDocument: TextDocument): PR | undefined { for (const registration of this._registrations.values()) { let selector = registration.data.registerOptions.documentSelector if (selector !== null && workspace.match(selector, textDocument) > 0) { return registration.provider } } return undefined } protected getAllProviders(): Iterable { const result: PR[] = [] for (const item of this._registrations.values()) { result.push(item.provider) } return result } } import { ProviderResult } from '../provider' import { defaultValue } from '../util' import { CodeLensProviderShape } from './codeLens' import { DiagnosticProviderShape } from './diagnostic' import { InlayHintsProviderShape } from './inlayHint' import { InlineValueProviderShape } from './inlineValue' import { SemanticTokensProviderShape } from './semanticTokens' import { DidChangeTextDocumentFeatureShape, DidCloseTextDocumentFeatureShape, DidOpenTextDocumentFeatureShape, DidSaveTextDocumentFeatureShape } from './textSynchronization' import { WorkspaceProviderFeature } from './workspaceSymbol' import { FoldingRangeProviderShape } from './foldingRange' export interface FeatureClient { clientOptions: CO middleware: M readonly id: string readonly configuredSection: string | undefined supportedMarkupKind: MarkupKind[] code2ProtocolConverter: c2p.Converter start(): Promise isRunning(): boolean stop(): Promise attachExtensionName(provider: T): void sendRequest(type: ProtocolRequestType0, token?: CancellationToken): Promise sendRequest(type: ProtocolRequestType, params: NoInfer>, token?: CancellationToken): Promise sendRequest(type: RequestType0, token?: CancellationToken): Promise sendRequest(type: RequestType, params: NoInfer>, token?: CancellationToken): Promise sendRequest(method: string, token?: CancellationToken): Promise sendRequest(method: string, param: any, token?: CancellationToken): Promise onRequest(type: ProtocolRequestType0, handler: NoInfer>): Disposable onRequest(type: ProtocolRequestType, handler: NoInfer>): Disposable onRequest(type: RequestType0, handler: NoInfer>): Disposable onRequest(type: RequestType, handler: NoInfer>): Disposable onRequest(method: string, handler: GenericRequestHandler): Disposable sendNotification(type: ProtocolNotificationType0): Promise sendNotification(type: ProtocolNotificationType, params?: NoInfer>): Promise sendNotification(type: NotificationType0): Promise sendNotification

(type: NotificationType

, params?: NoInfer>): Promise sendNotification(method: string, params?: any): Promise onNotification(type: ProtocolNotificationType0, handler: NotificationHandler0): Disposable onNotification(type: ProtocolNotificationType, handler: NoInfer>): Disposable onNotification(type: NotificationType0, handler: NotificationHandler0): Disposable onNotification

(type: NotificationType

, handler: NoInfer>): Disposable onNotification(method: string, handler: GenericNotificationHandler): Disposable onProgress

(type: ProgressType

, token: string | number, handler: NoInfer>): Disposable info(message: string, data?: any, showNotification?: boolean): void warn(message: string, data?: any, showNotification?: boolean): void error(message: string, data?: any, showNotification?: boolean | 'force'): void handleFailedRequest(type: MessageSignature, token: CancellationToken | undefined, error: any, defaultValue: T, showNotification?: boolean): T getFeature(request: typeof DidChangeWorkspaceFoldersNotification.method): DynamicFeature getFeature(request: typeof ExecuteCommandRequest.method): DynamicFeature getFeature(request: typeof DidChangeWatchedFilesNotification.method): DynamicFeature getFeature(request: typeof DidOpenTextDocumentNotification.method): DidOpenTextDocumentFeatureShape getFeature(request: typeof DidChangeTextDocumentNotification.method): DidChangeTextDocumentFeatureShape getFeature(request: typeof WillSaveTextDocumentNotification.method): DynamicFeature & TextDocumentSendFeature<(textDocument: TextDocumentWillSaveEvent) => Promise> getFeature(request: typeof WillSaveTextDocumentWaitUntilRequest.method): DynamicFeature & TextDocumentSendFeature<(textDocument: TextDocument) => ProviderResult> getFeature(request: typeof DidSaveTextDocumentNotification.method): DidSaveTextDocumentFeatureShape getFeature(request: typeof DidCloseTextDocumentNotification.method): DidCloseTextDocumentFeatureShape getFeature(request: typeof DidCreateFilesNotification.method): DynamicFeature & { send: (event: FileCreateEvent) => Promise } getFeature(request: typeof DidRenameFilesNotification.method): DynamicFeature & { send: (event: FileRenameEvent) => Promise } getFeature(request: typeof DidDeleteFilesNotification.method): DynamicFeature & { send: (event: FileDeleteEvent) => Promise } getFeature(request: typeof WillCreateFilesRequest.method): DynamicFeature & { send: (event: FileWillCreateEvent) => Promise } getFeature(request: typeof WillRenameFilesRequest.method): DynamicFeature & { send: (event: FileWillRenameEvent) => Promise } getFeature(request: typeof WillDeleteFilesRequest.method): DynamicFeature & { send: (event: FileWillDeleteEvent) => Promise } getFeature(request: typeof CompletionRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof HoverRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof SignatureHelpRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DefinitionRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof ReferencesRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentHighlightRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof CodeActionRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof CodeLensRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentFormattingRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentRangeFormattingRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentOnTypeFormattingRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof RenameRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentSymbolRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentLinkRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DocumentColorRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof DeclarationRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof FoldingRangeRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof ImplementationRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof SelectionRangeRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof TypeDefinitionRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof CallHierarchyPrepareRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof SemanticTokensRegistrationType.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof LinkedEditingRangeRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof TypeHierarchyPrepareRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof InlineCompletionRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof InlineValueRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof InlayHintRequest.method): DynamicFeature & TextDocumentProviderFeature getFeature(request: typeof WorkspaceSymbolRequest.method): DynamicFeature & WorkspaceProviderFeature getFeature(request: typeof DocumentDiagnosticRequest.method): DynamicFeature & TextDocumentProviderFeature | undefined // getFeature(request: typeof NotebookDocumentSyncRegistrationType.method): DynamicFeature & NotebookDocumentProviderShape | undefined; } ================================================ FILE: src/language-client/fileOperations.ts ================================================ 'use strict' import { Minimatch, MinimatchOptions } from 'minimatch' import type { ClientCapabilities, CreateFilesParams, DeleteFilesParams, Disposable, Event, FileOperationClientCapabilities, FileOperationOptions, FileOperationPatternOptions, FileOperationRegistrationOptions, ProtocolNotificationType, ProtocolRequestType, RegistrationType, RenameFilesParams, RequestParam, ServerCapabilities, WorkspaceEdit } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { FileCreateEvent, FileDeleteEvent, FileRenameEvent, FileWillCreateEvent, FileWillDeleteEvent, FileWillRenameEvent } from '../core/files' import { defaultValue } from '../util' import { FileType, getFileType } from '../util/fs' import { minimatch } from '../util/node' import { CancellationToken, DidCreateFilesNotification, DidDeleteFilesNotification, DidRenameFilesNotification, FileOperationPatternKind, WillCreateFilesRequest, WillDeleteFilesRequest, WillRenameFilesRequest } from '../util/protocol' import workspace from '../workspace' import { BaseFeature, DynamicFeature, ensure, FeatureClient, FeatureState, NextSignature, RegistrationData } from './features' import * as UUID from './utils/uuid' function access(target: T, key: K): T[K] { return target[key] } function assign(target: T, key: K, value: T[K]): void { target[key] = value } /** * File operation middleware * @since 3.16.0 */ export interface FileOperationsMiddleware { didCreateFiles?: NextSignature willCreateFiles?: NextSignature> didRenameFiles?: NextSignature willRenameFiles?: NextSignature> didDeleteFiles?: NextSignature willDeleteFiles?: NextSignature> } interface FileOperationsWorkspaceMiddleware { workspace?: FileOperationsMiddleware } interface EventWithFiles { readonly files: ReadonlyArray } abstract class FileOperationFeature> extends BaseFeature implements DynamicFeature { private _event: Event private _registrationType: RegistrationType private _clientCapability: keyof FileOperationClientCapabilities private _serverCapability: keyof FileOperationOptions private _listener: Disposable | undefined private _filters = new Map< string, Array<{ scheme?: string matcher: Minimatch kind?: FileOperationPatternKind }> >() constructor( client: FeatureClient, event: Event, registrationType: RegistrationType, clientCapability: keyof FileOperationClientCapabilities, serverCapability: keyof FileOperationOptions ) { super(client) this._event = event this._registrationType = registrationType this._clientCapability = clientCapability this._serverCapability = serverCapability } public getState(): FeatureState { return { kind: 'workspace', id: this._registrationType.method, registrations: this._filters.size > 0 } } public get registrationType(): RegistrationType { return this._registrationType } public fillClientCapabilities(capabilities: ClientCapabilities): void { const value = ensure(ensure(capabilities, 'workspace')!, 'fileOperations')! // this happens n times but it is the same value so we tolerate this. assign(value, 'dynamicRegistration', true) assign(value, this._clientCapability, true) } public initialize(capabilities: ServerCapabilities): void { const options = capabilities.workspace?.fileOperations const capability = options !== undefined ? access(options, this._serverCapability) : undefined if (capability?.filters !== undefined) { try { this.register({ id: UUID.generateUuid(), registerOptions: { filters: capability.filters }, }) } catch (e) { this._client.warn( `Ignoring invalid glob pattern for ${this._serverCapability} registration: ${e}` ) } } } public register(data: RegistrationData): void { if (!this._listener) { this._listener = this._event(this.send, this) } const minimatchFilter = data.registerOptions.filters.map(filter => { const matcher = new minimatch.Minimatch( filter.pattern.glob, FileOperationFeature.asMinimatchOptions(filter.pattern.options) ) if (!matcher.makeRe()) { throw new Error(`Invalid pattern ${filter.pattern.glob}!`) } return { scheme: filter.scheme, matcher, kind: filter.pattern.matches } }) this._filters.set(data.id, minimatchFilter) } public sendWithMiddleware(fn: (...args: any[]) => Promise | T, key: string, ...params: any[]): Promise | T { const middleware = defaultValue(defaultValue(this._client.middleware, {}).workspace, {}) return middleware[key] ? middleware[key](...params, fn) : fn(...params) } public abstract send(data: E): Promise public unregister(id: string): void { this._filters.delete(id) } public dispose(): void { this._filters.clear() if (this._listener) { this._listener.dispose() this._listener = undefined } } public async filter(event: E, prop: (i: I) => URI): Promise { // (Asynchronously) map each file onto a boolean of whether it matches // any of the globs. const fileMatches = await Promise.all( event.files.map(async item => { const uri = prop(item) // Use fsPath to make this consistent with file system watchers but help // minimatch to use '/' instead of `\\` if present. const path = uri.fsPath.replace(/\\/g, '/') for (const filters of this._filters.values()) { for (const filter of filters) { if (filter.scheme !== undefined && filter.scheme !== uri.scheme) { continue } if (filter.matcher.match(path)) { // The pattern matches. If kind is undefined then everything is ok if (filter.kind === undefined) { return true } const fileType = await getFileType(uri.fsPath) // If we can't determine the file type than we treat it as a match. // Dropping it would be another alternative. if (fileType === undefined) { this._client.error(`Failed to determine file type for ${uri.toString()}.`) return true } if ( (fileType === FileType.File && filter.kind === FileOperationPatternKind.file) || (fileType === FileType.Directory && filter.kind === FileOperationPatternKind.folder) ) { return true } } else if (filter.kind === FileOperationPatternKind.folder) { const fileType = await getFileType(uri.fsPath) if (fileType === FileType.Directory && filter.matcher.match(`${path}/`)) { return true } } } } return false }) ) // Filter the files to those that matched. const files = event.files.filter((_, index) => fileMatches[index]) return { ...event, files } } public static asMinimatchOptions(options: FileOperationPatternOptions | undefined): MinimatchOptions | undefined { if (options === undefined) { return undefined } if (options.ignoreCase === true) { return { nocase: true } } return undefined } } abstract class NotificationFileOperationFeature }, P> extends FileOperationFeature { private _notificationType: ProtocolNotificationType private _accessUri: (i: I) => URI private _createParams: (e: E) => RequestParam

constructor( client: FeatureClient, event: Event, notificationType: ProtocolNotificationType, clientCapability: keyof FileOperationClientCapabilities, serverCapability: keyof FileOperationOptions, accessUri: (i: I) => URI, createParams: (e: E) => RequestParam

) { super(client, event, notificationType, clientCapability, serverCapability) this._notificationType = notificationType this._accessUri = accessUri this._createParams = createParams } public async send(originalEvent: E): Promise { // Create a copy of the event that has the files filtered to match what the // server wants. const filteredEvent = await this.filter(originalEvent, this._accessUri) if (filteredEvent.files.length) { const next = async (event: E): Promise => { return this._client.sendNotification( this._notificationType, this._createParams(event) ) } let promise = this.doSend(filteredEvent, next) if (promise) { await promise.catch(e => { this._client.error(`Sending notification ${this.registrationType.method} failed`, e) }) } } } protected abstract doSend(event: E, next: (event: E) => void): void | Promise } export class DidCreateFilesFeature extends NotificationFileOperationFeature { constructor(client: FeatureClient) { super( client, workspace.onDidCreateFiles, DidCreateFilesNotification.type, 'didCreate', 'didCreate', (i: URI) => i, client.code2ProtocolConverter.asDidCreateFilesParams ) } protected doSend(event: FileCreateEvent, next: (event: FileCreateEvent) => void): void | Promise { return this.sendWithMiddleware(next, 'didCreateFiles', event) } } export class DidRenameFilesFeature extends NotificationFileOperationFeature<{ oldUri: URI; newUri: URI }, FileRenameEvent, RenameFilesParams> { constructor(client: FeatureClient) { super( client, workspace.onDidRenameFiles, DidRenameFilesNotification.type, 'didRename', 'didRename', (i: { oldUri: URI; newUri: URI }) => i.oldUri, client.code2ProtocolConverter.asDidRenameFilesParams ) } protected doSend(event: FileRenameEvent, next: (event: FileRenameEvent) => void): void | Promise { return this.sendWithMiddleware(next, 'didRenameFiles', event) } } export class DidDeleteFilesFeature extends NotificationFileOperationFeature { constructor(client: FeatureClient) { super( client, workspace.onDidDeleteFiles, DidDeleteFilesNotification.type, 'didDelete', 'didDelete', (i: URI) => i, client.code2ProtocolConverter.asDidDeleteFilesParams ) } protected doSend(event: FileCreateEvent, next: (event: FileCreateEvent) => void): void | Promise { return this.sendWithMiddleware(next, 'didDeleteFiles', event) } } interface RequestEvent { readonly files: ReadonlyArray waitUntil(thenable: Thenable): void } abstract class RequestFileOperationFeature, P> extends FileOperationFeature { private _requestType: ProtocolRequestType private _accessUri: (i: I) => URI private _createParams: (e: EventWithFiles) => RequestParam

constructor( client: FeatureClient, event: Event, requestType: ProtocolRequestType, clientCapability: keyof FileOperationClientCapabilities, serverCapability: keyof FileOperationOptions, accessUri: (i: I) => URI, createParams: (e: EventWithFiles) => RequestParam

) { super(client, event, requestType, clientCapability, serverCapability) this._requestType = requestType this._accessUri = accessUri this._createParams = createParams } public async send(originalEvent: E & RequestEvent): Promise { const waitUntil = this.waitUntil(originalEvent) originalEvent.waitUntil(waitUntil) } private async waitUntil(originalEvent: E): Promise { // Create a copy of the event that has the files filtered to match what the // server wants. const filteredEvent = await this.filter(originalEvent, this._accessUri) if (filteredEvent.files.length) { const next = (event: EventWithFiles): Promise => { return this.sendRequest(this._requestType, this._createParams(event), CancellationToken.None) } return this.doSend(filteredEvent, next) } else { return undefined } } protected abstract doSend(event: E, next: (event: EventWithFiles) => Thenable | Thenable): Thenable | Thenable } export class WillCreateFilesFeature extends RequestFileOperationFeature { constructor(client: FeatureClient) { super( client, workspace.onWillCreateFiles, WillCreateFilesRequest.type, 'willCreate', 'willCreate', (i: URI) => i, client.code2ProtocolConverter.asWillCreateFilesParams ) } protected doSend(event: FileWillCreateEvent, next: (event: FileWillCreateEvent) => Thenable | Thenable): Thenable | Thenable { return this.sendWithMiddleware(next, 'willCreateFiles', event) } } export class WillRenameFilesFeature extends RequestFileOperationFeature<{ oldUri: URI; newUri: URI }, FileWillRenameEvent, RenameFilesParams> { constructor(client: FeatureClient) { super( client, workspace.onWillRenameFiles, WillRenameFilesRequest.type, 'willRename', 'willRename', (i: { oldUri: URI; newUri: URI }) => i.oldUri, client.code2ProtocolConverter.asWillRenameFilesParams ) } protected doSend(event: FileWillRenameEvent, next: (event: FileWillRenameEvent) => Thenable | Thenable): Thenable | Thenable { return this.sendWithMiddleware(next, 'willRenameFiles', event) } } export class WillDeleteFilesFeature extends RequestFileOperationFeature { constructor(client: FeatureClient) { super( client, workspace.onWillDeleteFiles, WillDeleteFilesRequest.type, 'willDelete', 'willDelete', (i: URI) => i, client.code2ProtocolConverter.asWillDeleteFilesParams ) } protected doSend(event: FileWillDeleteEvent, next: (event: FileWillDeleteEvent) => Thenable | Thenable): Thenable | Thenable { return this.sendWithMiddleware(next, 'willDeleteFiles', event) } } ================================================ FILE: src/language-client/fileSystemWatcher.ts ================================================ 'use strict' import type { ClientCapabilities, DidChangeWatchedFilesRegistrationOptions, Disposable, DocumentSelector, FileEvent, RegistrationType, ServerCapabilities } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import RelativePatternImpl from '../model/relativePattern' import { GlobPattern, IFileSystemWatcher } from '../types' import { defaultValue, disposeAll } from '../util' import * as Is from '../util/is' import { DidChangeWatchedFilesNotification, FileChangeType, RelativePattern, WatchKind } from '../util/protocol' import workspace from '../workspace' import { DynamicFeature, ensure, FeatureClient, FeatureState, RegistrationData } from './features' import * as UUID from './utils/uuid' export interface DidChangeWatchedFileSignature { (this: void, event: FileEvent): void } interface $FileEventOptions { synchronize?: { fileEvents?: IFileSystemWatcher | IFileSystemWatcher[] } } export function asRelativePattern(rp: RelativePattern): RelativePatternImpl { let { baseUri, pattern } = rp if (typeof baseUri === 'string') { return new RelativePatternImpl(URI.parse(baseUri), pattern) } return new RelativePatternImpl(baseUri, pattern) } export class FileSystemWatcherFeature implements DynamicFeature { private _watchers: Map = new Map() private _fileEventsMap: Map = new Map() private readonly _client: FeatureClient private readonly _notifyFileEvent: (event: FileEvent) => void constructor(client: FeatureClient, notifyFileEvent: (event: FileEvent) => void) { this._client = client this._notifyFileEvent = notifyFileEvent this._watchers = new Map() } public getState(): FeatureState { return { kind: 'workspace', id: this.registrationType.method, registrations: this._watchers.size > 0 } } public get registrationType(): RegistrationType { return DidChangeWatchedFilesNotification.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'workspace')!, 'didChangeWatchedFiles')!.dynamicRegistration = true ensure(ensure(capabilities, 'workspace')!, 'didChangeWatchedFiles')!.relativePatternSupport = true } public initialize(_capabilities: ServerCapabilities, _documentSelector: DocumentSelector): void { let fileEvents = defaultValue(this._client.clientOptions.synchronize, {}).fileEvents if (!fileEvents) return let watchers: IFileSystemWatcher[] = Array.isArray(fileEvents) ? fileEvents : [fileEvents] let disposables: Disposable[] = [] for (let fileSystemWatcher of watchers) { disposables.push(fileSystemWatcher) this.hookListeners( fileSystemWatcher, !fileSystemWatcher.ignoreCreateEvents, !fileSystemWatcher.ignoreChangeEvents, !fileSystemWatcher.ignoreDeleteEvents, disposables) } this._watchers.set(UUID.generateUuid(), disposables) } public register(data: RegistrationData): void { if (!Array.isArray(data.registerOptions.watchers)) { return } let disposables: Disposable[] = [] for (let watcher of data.registerOptions.watchers) { let globPattern: GlobPattern if (Is.string(watcher.globPattern)) { globPattern = watcher.globPattern } else if (RelativePattern.is(watcher.globPattern)) { globPattern = asRelativePattern(watcher.globPattern) } else { continue } let watchCreate = true let watchChange = true let watchDelete = true if (watcher.kind != null) { watchCreate = (watcher.kind & WatchKind.Create) !== 0 watchChange = (watcher.kind & WatchKind.Change) !== 0 watchDelete = (watcher.kind & WatchKind.Delete) !== 0 } let fileSystemWatcher = workspace.createFileSystemWatcher( globPattern, !watchCreate, !watchChange, !watchDelete ) this.hookListeners( fileSystemWatcher, watchCreate, watchChange, watchDelete, disposables ) disposables.push(fileSystemWatcher) } this._watchers.set(data.id, disposables) } private hookListeners( fileSystemWatcher: IFileSystemWatcher, watchCreate: boolean, watchChange: boolean, watchDelete: boolean, listeners: Disposable[] ): void { const client = this._client // TODO rename support if (watchCreate) { fileSystemWatcher.onDidCreate( resource => this._notifyFileEvent({ uri: client.code2ProtocolConverter.asUri(resource), type: FileChangeType.Created }), null, listeners ) } if (watchChange) { fileSystemWatcher.onDidChange( resource => this._notifyFileEvent({ uri: client.code2ProtocolConverter.asUri(resource), type: FileChangeType.Changed }), null, listeners ) } if (watchDelete) { fileSystemWatcher.onDidDelete( resource => this._notifyFileEvent({ uri: client.code2ProtocolConverter.asUri(resource), type: FileChangeType.Deleted }), null, listeners ) } } public unregister(id: string): void { let disposables = this._watchers.get(id) if (disposables) { this._watchers.delete(id) disposeAll(disposables) } } public dispose(): void { this._fileEventsMap.clear() this._watchers.forEach(disposables => { disposeAll(disposables) }) this._watchers.clear() } } ================================================ FILE: src/language-client/foldingRange.ts ================================================ 'use strict' import { Emitter, FoldingRangeRefreshRequest, type CancellationToken, type ClientCapabilities, type Disposable, type DocumentSelector, type FoldingRange, type FoldingRangeOptions, type FoldingRangeParams, type FoldingRangeRegistrationOptions, type ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { FoldingContext, FoldingRangeProvider, ProviderResult } from '../provider' import { FoldingRangeKind, FoldingRangeRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export type ProvideFoldingRangeSignature = ( this: void, document: TextDocument, context: FoldingContext, token: CancellationToken ) => ProviderResult export interface FoldingRangeProviderMiddleware { provideFoldingRanges?: ( this: void, document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature ) => ProviderResult } export interface FoldingRangeProviderShape { provider: FoldingRangeProvider; onDidChangeFoldingRange: Emitter; } export class FoldingRangeFeature extends TextDocumentLanguageFeature< boolean | FoldingRangeOptions, FoldingRangeRegistrationOptions, FoldingRangeProviderShape, FoldingRangeProviderMiddleware > { constructor(client: FeatureClient) { super(client, FoldingRangeRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let capability = ensure(ensure(capabilities, 'textDocument')!, 'foldingRange')! capability.dynamicRegistration = true capability.rangeLimit = 5000 capability.lineFoldingOnly = true capability.foldingRangeKind = { valueSet: [FoldingRangeKind.Comment, FoldingRangeKind.Imports, FoldingRangeKind.Region] } capability.foldingRange = { collapsedText: false } ensure(ensure(capabilities, 'workspace')!, 'foldingRange')!.refreshSupport = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { this._client.onRequest(FoldingRangeRefreshRequest.type, async () => { for (const provider of this.getAllProviders()) { provider.onDidChangeFoldingRange.fire() } }) const [id, options] = this.getRegistration(documentSelector, capabilities.foldingRangeProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider( options: FoldingRangeRegistrationOptions ): [Disposable, FoldingRangeProviderShape] { const eventEmitter: Emitter = new Emitter() const provider: FoldingRangeProvider = { onDidChangeFoldingRanges: eventEmitter.event, provideFoldingRanges: (document, context, token) => { const client = this._client const provideFoldingRanges: ProvideFoldingRangeSignature = (document, _, token) => { const requestParams: FoldingRangeParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) } return this.sendRequest(FoldingRangeRequest.type, requestParams, token) } const middleware = client.middleware return middleware.provideFoldingRanges ? middleware.provideFoldingRanges(document, context, token, provideFoldingRanges) : provideFoldingRanges(document, context, token) } } this._client.attachExtensionName(provider) return [languages.registerFoldingRangeProvider(options.documentSelector, provider), { provider, onDidChangeFoldingRange: eventEmitter }] } } ================================================ FILE: src/language-client/formatting.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentFormattingOptions, DocumentFormattingParams, DocumentFormattingRegistrationOptions, DocumentOnTypeFormattingOptions, DocumentOnTypeFormattingParams, DocumentOnTypeFormattingRegistrationOptions, DocumentRangeFormattingOptions, DocumentRangeFormattingParams, DocumentRangeFormattingRegistrationOptions, DocumentSelector, FormattingOptions, Position, Range, ServerCapabilities, TextDocumentRegistrationOptions, TextEdit } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider, OnTypeFormattingEditProvider, ProviderResult } from '../provider' import { DocumentFormattingRequest, DocumentOnTypeFormattingRequest, DocumentRangeFormattingRequest } from '../util/protocol' import { FeatureClient, TextDocumentLanguageFeature, ensure } from './features' import * as UUID from './utils/uuid' export interface ProvideDocumentFormattingEditsSignature { ( this: void, document: TextDocument, options: FormattingOptions, token: CancellationToken ): ProviderResult } export interface ProvideDocumentRangeFormattingEditsSignature { ( this: void, document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken ): ProviderResult } export interface ProvideOnTypeFormattingEditsSignature { ( this: void, document: TextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken ): ProviderResult } export interface $FormattingOptions { formatterPriority?: number } export interface FormattingMiddleware { provideDocumentFormattingEdits?: ( this: void, document: TextDocument, options: FormattingOptions, token: CancellationToken, next: ProvideDocumentFormattingEditsSignature ) => ProviderResult provideDocumentRangeFormattingEdits?: ( this: void, document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken, next: ProvideDocumentRangeFormattingEditsSignature ) => ProviderResult provideOnTypeFormattingEdits?: ( this: void, document: TextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken, next: ProvideOnTypeFormattingEditsSignature ) => ProviderResult } export class DocumentFormattingFeature extends TextDocumentLanguageFeature< boolean | DocumentFormattingOptions, DocumentFormattingRegistrationOptions, DocumentFormattingEditProvider, FormattingMiddleware, $FormattingOptions > { constructor(client: FeatureClient) { super(client, DocumentFormattingRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure( ensure(capabilities, 'textDocument')!, 'formatting' )!.dynamicRegistration = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.documentFormattingProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: TextDocumentRegistrationOptions ): [Disposable, DocumentFormattingEditProvider] { const provider: DocumentFormattingEditProvider = { provideDocumentFormattingEdits: (document, options, token) => { const client = this._client const provideDocumentFormattingEdits: ProvideDocumentFormattingEditsSignature = (document, options, token) => { const params: DocumentFormattingParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), options } return this.sendRequest(DocumentFormattingRequest.type, params, token) } const middleware = client.middleware! return middleware.provideDocumentFormattingEdits ? middleware.provideDocumentFormattingEdits(document, options, token, provideDocumentFormattingEdits) : provideDocumentFormattingEdits(document, options, token) } } this._client.attachExtensionName(provider) return [ languages.registerDocumentFormatProvider(options.documentSelector!, provider, this._client.clientOptions.formatterPriority), provider ] } } export class DocumentRangeFormattingFeature extends TextDocumentLanguageFeature< boolean | DocumentRangeFormattingOptions, DocumentRangeFormattingRegistrationOptions, DocumentRangeFormattingEditProvider, FormattingMiddleware, $FormattingOptions > { constructor(client: FeatureClient) { super(client, DocumentRangeFormattingRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure( ensure(capabilities, 'textDocument')!, 'rangeFormatting' )!.dynamicRegistration = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.documentRangeFormattingProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: TextDocumentRegistrationOptions ): [Disposable, DocumentRangeFormattingEditProvider] { const provider: DocumentRangeFormattingEditProvider = { provideDocumentRangeFormattingEdits: (document, range, options, token) => { const client = this._client const provideDocumentRangeFormattingEdits: ProvideDocumentRangeFormattingEditsSignature = (document, range, options, token) => { const params: DocumentRangeFormattingParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range, options, } return this.sendRequest(DocumentRangeFormattingRequest.type, params, token) } const middleware = client.middleware! return middleware.provideDocumentRangeFormattingEdits ? middleware.provideDocumentRangeFormattingEdits(document, range, options, token, provideDocumentRangeFormattingEdits) : provideDocumentRangeFormattingEdits(document, range, options, token) } } this._client.attachExtensionName(provider) return [ languages.registerDocumentRangeFormatProvider(options.documentSelector, provider, this._client.clientOptions.formatterPriority), provider ] } } export class DocumentOnTypeFormattingFeature extends TextDocumentLanguageFeature< DocumentOnTypeFormattingOptions, DocumentOnTypeFormattingRegistrationOptions, OnTypeFormattingEditProvider, FormattingMiddleware > { constructor(client: FeatureClient) { super(client, DocumentOnTypeFormattingRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'onTypeFormatting')!.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const options = this.getRegistrationOptions(documentSelector, capabilities.documentOnTypeFormattingProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider(options: DocumentOnTypeFormattingRegistrationOptions): [Disposable, OnTypeFormattingEditProvider] { const provider: OnTypeFormattingEditProvider = { provideOnTypeFormattingEdits: (document, position, ch, options, token) => { const client = this._client const provideOnTypeFormattingEdits: ProvideOnTypeFormattingEditsSignature = (document, position, ch, options, token) => { const params: DocumentOnTypeFormattingParams = { textDocument: client.code2ProtocolConverter.asVersionedTextDocumentIdentifier(document), position, ch, options } return this.sendRequest(DocumentOnTypeFormattingRequest.type, params, token) } const middleware = client.middleware! return middleware.provideOnTypeFormattingEdits ? middleware.provideOnTypeFormattingEdits(document, position, ch, options, token, provideOnTypeFormattingEdits) : provideOnTypeFormattingEdits(document, position, ch, options, token) } } this._client.attachExtensionName(provider) const moreTriggerCharacter = options.moreTriggerCharacter || [] const characters = [options.firstTriggerCharacter, ...moreTriggerCharacter] return [languages.registerOnTypeFormattingEditProvider(options.documentSelector!, provider, characters), provider] } } ================================================ FILE: src/language-client/hover.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, Hover, HoverOptions, HoverRegistrationOptions, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { HoverProvider, ProviderResult } from '../provider' import { HoverRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideHoverSignature { ( this: void, document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } export interface HoverMiddleware { provideHover?: ( this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature ) => ProviderResult } export class HoverFeature extends TextDocumentLanguageFeature< boolean | HoverOptions, HoverRegistrationOptions, HoverProvider, HoverMiddleware > { constructor(client: FeatureClient) { super(client, HoverRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const hoverCapability = ensure( ensure(capabilities, 'textDocument')!, 'hover' )! hoverCapability.dynamicRegistration = true hoverCapability.contentFormat = this._client.supportedMarkupKind } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.hoverProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: HoverRegistrationOptions ): [Disposable, HoverProvider] { const provider: HoverProvider = { provideHover: (document, position, token) => { const client = this._client const provideHover: ProvideHoverSignature = (document, position, token) => { return this.sendRequest( HoverRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token ) } const middleware = client.middleware! return middleware.provideHover ? middleware.provideHover(document, position, token, provideHover) : provideHover(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerHoverProvider(options.documentSelector!, provider), provider] } } ================================================ FILE: src/language-client/implementation.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Definition, DefinitionLink, Disposable, DocumentSelector, ImplementationOptions, ImplementationRegistrationOptions, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { ImplementationProvider, ProviderResult } from '../provider' import { ImplementationRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface ProvideImplementationSignature { (this: void, document: TextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ImplementationMiddleware { provideImplementation?: (this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideImplementationSignature) => ProviderResult } export class ImplementationFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, ImplementationRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const implementationSupport = ensure(ensure(capabilities, 'textDocument')!, 'implementation')! implementationSupport.dynamicRegistration = true implementationSupport.linkSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const [id, options] = this.getRegistration(documentSelector, capabilities.implementationProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: ImplementationRegistrationOptions): [Disposable, ImplementationProvider] { const provider: ImplementationProvider = { provideImplementation: (document, position, token) => { const client = this._client const provideImplementation: ProvideImplementationSignature = (document, position, token) => this.sendRequest(ImplementationRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token) const middleware = client.middleware return middleware.provideImplementation ? middleware.provideImplementation(document, position, token, provideImplementation) : provideImplementation(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerImplementationProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/index.ts ================================================ /* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ 'use strict' /* eslint-disable no-redeclare */ import { ForkOptions as CForkOptions, ChildProcess, ChildProcessWithoutNullStreams, SpawnOptions } from 'child_process' import * as stream from 'stream' import { MessageReader, MessageWriter } from 'vscode-languageserver-protocol/node' import { URI } from 'vscode-uri' import { createLogger } from '../logger' import { OutputChannel } from '../types' import { disposeAll, getConditionValue } from '../util' import * as Is from '../util/is' import { child_process, fs, path, readline } from '../util/node' import { terminate } from '../util/processes' import { Disposable, generateRandomPipeName, IPCMessageReader, IPCMessageWriter, StreamMessageReader, StreamMessageWriter } from '../util/protocol' import workspace from '../workspace' import { BaseLanguageClient, LanguageClientOptions, MessageTransports, ShutdownMode } from './client' import { createClientPipeTransport, createClientSocketTransport, currentTimeStamp } from './utils' const logger = createLogger('language-client-index') const debugStartWith: string[] = ['--debug=', '--debug-brk=', '--inspect=', '--inspect-brk='] const debugEquals: string[] = ['--debug', '--debug-brk', '--inspect', '--inspect-brk'] const STOP_TIMEOUT = getConditionValue(2000, 10) const RESTART_TIMEOUT = getConditionValue(1000, 10) export * from './client' declare let v8debug: any export enum TransportKind { stdio, ipc, pipe, socket } export interface SocketTransport { kind: TransportKind.socket port: number } export interface DisposableTransport { onConnected(): Promise<[MessageReader, MessageWriter]> dispose(): void } namespace Transport { export function isSocket(value: Transport): value is SocketTransport { let candidate = value as SocketTransport return ( candidate && candidate.kind === TransportKind.socket && Is.number(candidate.port) ) } } /** * To avoid any timing, pipe name or port number issues the pipe (TransportKind.pipe) * and the sockets (TransportKind.socket and SocketTransport) are owned by the * VS Code processes. The server process simply connects to the pipe / socket. * In node term the VS Code process calls `createServer`, then starts the server * process, waits until the server process has connected to the pipe / socket * and then signals that the connection has been established and messages can * be send back and forth. If the language server is implemented in a different * program language the server simply needs to create a connection to the * passed pipe name or port number. */ export type Transport = TransportKind | SocketTransport export interface ExecutableOptions { cwd?: string env?: any detached?: boolean shell?: boolean } export interface Executable { command: string transport?: Transport args?: string[] options?: ExecutableOptions } namespace Executable { export function is(value: any): value is Executable { return Is.string(value.command) } } export interface ForkOptions { cwd?: string env?: any execPath?: string encoding?: string execArgv?: string[] } export interface NodeModule { module: string transport?: Transport args?: string[] runtime?: string options?: ForkOptions } namespace NodeModule { export function is(value: any): value is NodeModule { return Is.string(value.module) } } export interface StreamInfo { writer: NodeJS.WritableStream reader: NodeJS.ReadableStream detached?: boolean } namespace StreamInfo { export function is(value: any): value is StreamInfo { let candidate = value as StreamInfo return ( candidate && candidate.writer !== void 0 && candidate.reader !== void 0 ) } } export interface ChildProcessInfo { process: ChildProcess detached: boolean } namespace ChildProcessInfo { export function is(value: any): value is ChildProcessInfo { let candidate = value as ChildProcessInfo return ( candidate && candidate.process !== void 0 && typeof candidate.detached === 'boolean' ) } } export type ServerOptions = | Executable | { run: Executable; debug: Executable } | { run: NodeModule; debug: NodeModule } | NodeModule | (() => Promise) export class LanguageClient extends BaseLanguageClient { private _forceDebug: boolean private _isInDebugMode: boolean private _serverProcess: ChildProcess | undefined private _isDetached: boolean | undefined private _serverOptions: ServerOptions public constructor( name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean ) public constructor( id: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean ) public constructor( arg1: string, arg2: string | ServerOptions, arg3: LanguageClientOptions | ServerOptions, arg4?: boolean | LanguageClientOptions, arg5?: boolean ) { let id: string let name: string let serverOptions: ServerOptions let clientOptions: LanguageClientOptions let forceDebug: boolean if (Is.string(arg2)) { id = arg1 name = arg2 serverOptions = arg3 as ServerOptions clientOptions = arg4 as LanguageClientOptions forceDebug = !!arg5 } else { // first signature id = arg1.toLowerCase() name = arg1 serverOptions = arg2 clientOptions = arg3 as LanguageClientOptions forceDebug = arg4 as boolean } super(id, name, clientOptions) this._serverOptions = serverOptions this._forceDebug = !!forceDebug this._isInDebugMode = !!forceDebug } protected shutdown(mode: ShutdownMode, timeout: number): Promise { return super.shutdown(mode, timeout).then(() => { if (this._serverProcess) { let toCheck = this._serverProcess this._serverProcess = undefined if (this._isDetached === void 0 || !this._isDetached) { checkProcessDied(toCheck) } this._isDetached = undefined } }, err => { if (this._serverProcess && err.message.includes('timed out')) { this._serverProcess.kill('SIGKILL') this._serverProcess = undefined return } throw err }) } public get serviceState() { return this._state } protected handleConnectionClosed(): Promise { this._serverProcess = undefined return super.handleConnectionClosed() } public get isInDebugMode(): boolean { return this._isInDebugMode } public async restart(): Promise { await this.stop() // We are in debug mode. Wait a little before we restart // so that the debug port can be freed. We can safely ignore // the disposable returned from start since it will call // stop on the same client instance. if (this.isInDebugMode) { await new Promise(resolve => setTimeout(resolve, RESTART_TIMEOUT)) await this._start() } else { await this._start() } } protected createMessageTransports(encoding: string): Promise { function getEnvironment(env: any, fork: boolean): any { if (!env && !fork) { return undefined } let result: any = Object.create(null) Object.keys(process.env).forEach(key => result[key] = process.env[key]) if (env) { Object.keys(env).forEach(key => result[key] = env[key]) } return result } function assertStdio(process: ChildProcess): asserts process is ChildProcessWithoutNullStreams { if (process.stdin === null || process.stdout === null || process.stderr === null) { process.kill('SIGKILL') throw new Error('Process created without stdio streams') } } function logMessage(kind: string, data: string, outputChannel: OutputChannel): void { let msg = `[${kind} - ${currentTimeStamp()}] ${data}` outputChannel.appendLine(msg) } function pipeStdoutToLogOutputChannel(input: stream.Readable, outputChannel: OutputChannel) { readline.createInterface({ input, crlfDelay: Infinity, terminal: false, historySize: 0, }).on('line', data => logMessage('Stdout', data, outputChannel)) } function pipeStderrToLogOutputChannel(input: stream.Readable, outputChannel: OutputChannel) { readline.createInterface({ input, crlfDelay: Infinity, terminal: false, historySize: 0, }).on('line', data => logMessage('Stderr', data, outputChannel)) } let server = this._serverOptions // We got a function. if (Is.func(server)) { return server().then(result => { if (MessageTransports.is(result)) { this._isDetached = !!result.detached return result } else if (StreamInfo.is(result)) { this._isDetached = !!result.detached return { reader: new StreamMessageReader(result.reader), writer: new StreamMessageWriter(result.writer) } } else { let cp: ChildProcess if (ChildProcessInfo.is(result)) { cp = result.process this._isDetached = result.detached } else { cp = result this._isDetached = false } pipeStderrToLogOutputChannel(cp.stderr, this.outputChannel) return { reader: new StreamMessageReader(cp.stdout!), writer: new StreamMessageWriter(cp.stdin!) } } }) } let json: NodeModule | Executable let runDebug = server as { run: any; debug: any } if (runDebug.run || runDebug.debug) { if (typeof v8debug === 'object' || this._forceDebug || startedInDebugMode(process.execArgv)) { json = runDebug.debug this._isInDebugMode = true } else { json = runDebug.run this._isInDebugMode = false } } else { json = server as NodeModule | Executable } return getServerWorkingDir(json.options).then(serverWorkingDir => { if (NodeModule.is(json) && json.module) { let node = json let transport = node.transport || TransportKind.stdio let pipeName: string | undefined let runtime = node.runtime ? getRuntimePath(node.runtime, serverWorkingDir) : undefined return new Promise((resolve, _reject) => { let args = node.args && node.args.slice() || [] if (transport === TransportKind.ipc) { args.push('--node-ipc') } else if (transport === TransportKind.stdio) { args.push('--stdio') } else if (transport === TransportKind.pipe) { pipeName = generateRandomPipeName() args.push(`--pipe=${pipeName}`) } else if (Transport.isSocket(transport)) { args.push(`--socket=${transport.port}`) } args.push(`--clientProcessId=${process.pid}`) let options: CForkOptions = node.options || Object.create(null) options.env = getEnvironment(options.env, true) options.execArgv = options.execArgv || [] options.cwd = serverWorkingDir options.silent = true if (runtime) options.execPath = runtime if (transport === TransportKind.ipc || transport === TransportKind.stdio) { // options.stdio = 'ignore' let sp = child_process.fork(node.module, args, options) assertStdio(sp) this._serverProcess = sp logger.info(`Language server "${this.id}" started with ${sp.pid}`) pipeStderrToLogOutputChannel(sp.stderr, this.outputChannel) if (transport === TransportKind.ipc) { pipeStdoutToLogOutputChannel(sp.stdout, this.outputChannel) resolve({ reader: new IPCMessageReader(this._serverProcess), writer: new IPCMessageWriter(this._serverProcess) }) } else { resolve({ reader: new StreamMessageReader(sp.stdout), writer: new StreamMessageWriter(sp.stdin) }) } } else if (transport === TransportKind.pipe) { return createClientPipeTransport(pipeName!).then(transport => { let sp = child_process.fork(node.module, args, options) assertStdio(sp) logger.info(`Language server "${this.id}" started with ${sp.pid}`) this._serverProcess = sp pipeStderrToLogOutputChannel(sp.stderr, this.outputChannel) pipeStdoutToLogOutputChannel(sp.stdout, this.outputChannel) void transport.onConnected().then(protocol => { resolve({ reader: protocol[0], writer: protocol[1] }) }) }) } else if (Transport.isSocket(transport)) { return createClientSocketTransport(transport.port).then(transport => { let sp = child_process.fork(node.module, args, options) assertStdio(sp) this._serverProcess = sp logger.info(`Language server "${this.id}" started with ${sp.pid}`) pipeStderrToLogOutputChannel(sp.stderr, this.outputChannel) pipeStdoutToLogOutputChannel(sp.stdout, this.outputChannel) void transport.onConnected().then(protocol => { resolve({ reader: protocol[0], writer: protocol[1] }) }) }) } }) } else if (Executable.is(json) && json.command) { let command: Executable = json let args = Array.isArray(command.args) ? command.args.slice(0) : [] let pipeName: string | undefined const transport = json.transport if (transport === TransportKind.stdio) { args.push('--stdio') } else if (transport === TransportKind.pipe) { pipeName = generateRandomPipeName() args.push(`--pipe=${pipeName}`) } else if (Transport.isSocket(transport)) { args.push(`--socket=${transport.port}`) } else if (transport === TransportKind.ipc) { throw new Error(`Transport kind ipc is not supported for command executable`) } let options = Object.assign({ shell: process.platform === 'win32' }, command.options) as SpawnOptions options.env = getEnvironment(options.env, false) options.cwd = options.cwd ?? serverWorkingDir options.windowsHide = true const attachProcess = (serverProcess: ChildProcess, pipiStdout = true) => { this._serverProcess = serverProcess this._isDetached = !!options.detached logger.info(`Language server "${this.id}" started with ${serverProcess.pid}`) if (pipiStdout) pipeStdoutToLogOutputChannel(serverProcess.stdout, this.outputChannel) pipeStderrToLogOutputChannel(serverProcess.stderr, this.outputChannel) } let cmd = workspace.expand(json.command) if (transport === undefined || transport === TransportKind.stdio) { const serverProcess = child_process.spawn(cmd, args, options) if (!serverProcess || !serverProcess.pid) { return handleChildProcessStartError(serverProcess, `Launching server using command ${cmd} failed.`) } attachProcess(serverProcess, false) return Promise.resolve({ reader: new StreamMessageReader(serverProcess.stdout), writer: new StreamMessageWriter(serverProcess.stdin) }) } else if (transport === TransportKind.pipe || Transport.isSocket(transport)) { let promise: Promise if (transport === TransportKind.pipe) { promise = createClientPipeTransport(pipeName!) } else { promise = createClientSocketTransport(transport.port) } return promise.then(transport => { const serverProcess = child_process.spawn(cmd, args, options) if (!serverProcess || !serverProcess.pid) { transport.dispose() return handleChildProcessStartError(serverProcess, `Launching server using command ${cmd} failed.`) } attachProcess(serverProcess) return transport.onConnected().then(protocol => { return { reader: protocol[0], writer: protocol[1] } }) }) } } return Promise.reject(new Error(`Unsupported server configuration ${JSON.stringify(server, null, 2)}`)) }).finally(() => { if (this._serverProcess !== undefined) { this._serverProcess.on('exit', (code, signal) => { if (code === 0) { this.info('Server process exited successfully', undefined, false) } else if (code !== null) { this.error(`Server process exited with code ${code}.`, undefined, false) } if (signal !== null) { this.error(`Server process exited with signal ${signal}.`, undefined, false) } }) } }) } } export class SettingMonitor { private _listeners: Disposable[] constructor(private _client: LanguageClient, private _setting: string) { this._listeners = [] } public start(): Disposable { workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(this._setting)) { this.onDidChangeConfiguration() } }, null, this._listeners) this.onDidChangeConfiguration() return { dispose: () => { disposeAll(this._listeners) void this._client.dispose() } } } private onDidChangeConfiguration(): void { let index = this._setting.indexOf('.') let primary = index >= 0 ? this._setting.substr(0, index) : this._setting let rest = index >= 0 ? this._setting.substr(index + 1) : undefined let enabled = rest ? workspace.getConfiguration(primary).get(rest, true) : workspace.getConfiguration().get(primary, true) if (enabled && this._client.needsStart()) { this._client.start().catch(error => this._client.error('Start failed after configuration change', error, 'force')) } else if (!enabled && this._client.needsStop()) { this._client.stop().catch(error => this._client.error('Stop failed after configuration change', error, 'force')) } } } export function getRuntimePath(runtime: string, serverWorkingDirectory: string | undefined): string { if (path.isAbsolute(runtime)) { return runtime } const mainRootPath = mainGetRootPath() if (mainRootPath !== undefined) { const result = path.join(mainRootPath, runtime) if (fs.existsSync(result)) { return result } } if (serverWorkingDirectory !== undefined) { const result = path.join(serverWorkingDirectory, runtime) if (fs.existsSync(result)) { return result } } return runtime } export function mainGetRootPath(): string | undefined { let folders = workspace.workspaceFolders if (!folders || folders.length === 0) { return undefined } return URI.parse(folders[0].uri).fsPath } export function getServerWorkingDir(options?: { cwd?: string }): Promise { let cwd = options && options.cwd if (cwd && !path.isAbsolute(cwd)) cwd = path.join(workspace.cwd, cwd) if (!cwd) cwd = workspace.cwd // make sure the folder exists otherwise creating the process will fail return new Promise(s => { fs.lstat(cwd, (err, stats) => { s(!err && stats.isDirectory() ? cwd : undefined) }) }) } export function startedInDebugMode(args: string[] | undefined): boolean { if (args) { return args.some(arg => { return debugStartWith.some(value => arg.startsWith(value)) || debugEquals.some(value => arg === value) }) } return false } export function handleChildProcessStartError(childProcess: ChildProcess, message: string) { if (childProcess === null) { return Promise.reject(message) } childProcess.unref() return new Promise((_, reject) => { childProcess.on('error', err => { reject(`${message} ${err}`) }) // the error event should always be run immediately, // but race on it just in case setImmediate(() => reject(message)) }) } export function checkProcessDied(childProcess: ChildProcess | undefined): void { if (!childProcess || childProcess.pid === undefined) return setTimeout(() => { // Test if the process is still alive. Throws an exception if not try { process.kill(childProcess.pid, 0) terminate(childProcess) } catch (error) { // All is fine. } }, STOP_TIMEOUT) } ================================================ FILE: src/language-client/inlayHint.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, InlayHint, InlayHintOptions, InlayHintParams, InlayHintRegistrationOptions, Range, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { InlayHintsProvider, ProviderResult } from '../provider' import { Emitter, InlayHintRefreshRequest, InlayHintRequest, InlayHintResolveRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export type ProvideInlayHintsSignature = (this: void, document: TextDocument, viewPort: Range, token: CancellationToken) => ProviderResult export type ResolveInlayHintSignature = (this: void, item: InlayHint, token: CancellationToken) => ProviderResult export interface InlayHintsMiddleware { provideInlayHints?: (this: void, document: TextDocument, viewPort: Range, token: CancellationToken, next: ProvideInlayHintsSignature) => ProviderResult resolveInlayHint?: (this: void, item: InlayHint, token: CancellationToken, next: ResolveInlayHintSignature) => ProviderResult } export interface InlayHintsProviderShape { provider: InlayHintsProvider onDidChangeInlayHints: Emitter } export class InlayHintsFeature extends TextDocumentLanguageFeature< boolean | InlayHintOptions, InlayHintRegistrationOptions, InlayHintsProviderShape, InlayHintsMiddleware > { constructor(client: FeatureClient) { super(client, InlayHintRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const inlayHint = ensure(ensure(capabilities, 'textDocument')!, 'inlayHint')! inlayHint.dynamicRegistration = true inlayHint.resolveSupport = { properties: ['tooltip', 'textEdits', 'label.tooltip', 'label.location', 'label.command'] } ensure(ensure(capabilities, 'workspace')!, 'inlayHint')!.refreshSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { this._client.onRequest(InlayHintRefreshRequest.type, async () => { for (const provider of this.getAllProviders()) { provider.onDidChangeInlayHints.fire() } }) const [id, options] = this.getRegistration(documentSelector, capabilities.inlayHintProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: InlayHintRegistrationOptions): [Disposable, InlayHintsProviderShape] { const eventEmitter: Emitter = new Emitter() const provider: InlayHintsProvider = { onDidChangeInlayHints: eventEmitter.event, provideInlayHints: (document, range, token) => { const client = this._client const provideInlayHints: ProvideInlayHintsSignature = (document, range, token) => { const requestParams: InlayHintParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range } return this.sendRequest(InlayHintRequest.type, requestParams, token, null) } const middleware = client.middleware! return middleware.provideInlayHints ? middleware.provideInlayHints(document, range, token, provideInlayHints) : provideInlayHints(document, range, token) } } provider.resolveInlayHint = options.resolveProvider === true ? (hint, token) => { const resolveInlayHint: ResolveInlayHintSignature = (item, token) => { return this.sendRequest(InlayHintResolveRequest.type, item, token) } return this.sendWithMiddleware(resolveInlayHint, 'resolveInlayHint', hint, token) } : undefined const selector = options.documentSelector! this._client.attachExtensionName(provider) return [languages.registerInlayHintsProvider(selector, provider), { provider, onDidChangeInlayHints: eventEmitter }] } } ================================================ FILE: src/language-client/inlineCompletion.ts ================================================ import { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, InlineCompletionContext, InlineCompletionItem, InlineCompletionList, InlineCompletionOptions, InlineCompletionParams, InlineCompletionRegistrationOptions, InlineCompletionRequest, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { InlineCompletionItemProvider, ProviderResult } from '../provider' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideInlineCompletionItemsSignature { (this: void, document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult } export interface InlineCompletionMiddleware { provideInlineCompletionItems?: (this: void, document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken, next: ProvideInlineCompletionItemsSignature) => ProviderResult } export interface InlineCompletionProviderShape { provider: InlineCompletionItemProvider } export class InlineCompletionItemFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, InlineCompletionRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const inlineCompletion = ensure(ensure(capabilities, 'textDocument')!, 'inlineCompletion')! inlineCompletion.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const options = this.getRegistrationOptions(documentSelector, capabilities.inlineCompletionProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider(options: InlineCompletionRegistrationOptions): [Disposable, InlineCompletionItemProvider] { const provider: InlineCompletionItemProvider = { provideInlineCompletionItems: (document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult => { const provideInlineCompletionItems: ProvideInlineCompletionItemsSignature = (document, position, context, token) => { const params: InlineCompletionParams = { textDocument: { uri: document.uri }, position, context } return this.sendRequest(InlineCompletionRequest.type, params, token, null) } const middleware = this._client.middleware return middleware.provideInlineCompletionItems ? middleware.provideInlineCompletionItems(document, position, context, token, provideInlineCompletionItems) : provideInlineCompletionItems(document, position, context, token) } } this._client.attachExtensionName(provider) return [languages.registerInlineCompletionItemProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/inlineValue.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, InlineValue, InlineValueContext, InlineValueOptions, InlineValueParams, InlineValueRegistrationOptions, Range, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { InlineValuesProvider, ProviderResult } from '../provider' import { Emitter, InlineValueRefreshRequest, InlineValueRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export type ProvideInlineValuesSignature = (this: void, document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken) => ProviderResult export interface InlineValueMiddleware { provideInlineValues?: (this: void, document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken, next: ProvideInlineValuesSignature) => ProviderResult } export interface InlineValueProviderShape { provider: InlineValuesProvider onDidChangeInlineValues: Emitter } export class InlineValueFeature extends TextDocumentLanguageFeature< boolean | InlineValueOptions, InlineValueRegistrationOptions, InlineValueProviderShape, InlineValueMiddleware > { constructor(client: FeatureClient) { super(client, InlineValueRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'inlineValue')!.dynamicRegistration = true ensure(ensure(capabilities, 'workspace')!, 'inlineValue')!.refreshSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { this._client.onRequest(InlineValueRefreshRequest.type, async () => { for (const provider of this.getAllProviders()) { provider.onDidChangeInlineValues.fire() } }) const [id, options] = this.getRegistration(documentSelector, capabilities.inlineValueProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: InlineValueRegistrationOptions): [Disposable, InlineValueProviderShape] { const eventEmitter: Emitter = new Emitter() const provider: InlineValuesProvider = { onDidChangeInlineValues: eventEmitter.event, provideInlineValues: (document, viewPort, context, token) => { const client = this._client const provideInlineValues: ProvideInlineValuesSignature = (document, range, context, token) => { const requestParams: InlineValueParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range, context } return this.sendRequest(InlineValueRequest.type, requestParams, token) } const middleware = client.middleware! return middleware.provideInlineValues ? middleware.provideInlineValues(document, viewPort, context, token, provideInlineValues) : provideInlineValues(document, viewPort, context, token) } } this._client.attachExtensionName(provider) const selector = options.documentSelector! return [languages.registerInlineValuesProvider(selector, provider), { provider, onDidChangeInlineValues: eventEmitter }] } } ================================================ FILE: src/language-client/linkedEditingRange.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, LinkedEditingRangeOptions, LinkedEditingRangeRegistrationOptions, LinkedEditingRanges, Position, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { LinkedEditingRangeProvider, ProviderResult } from '../provider' import { LinkedEditingRangeRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface ProvideLinkedEditingRangeSignature { (this: void, document: TextDocument, position: Position, token: CancellationToken): ProviderResult } /** * Linked editing middleware * @since 3.16.0 */ export interface LinkedEditingRangeMiddleware { provideLinkedEditingRange?: (this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideLinkedEditingRangeSignature) => ProviderResult } export class LinkedEditingFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, LinkedEditingRangeRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const linkedEditingSupport = ensure(ensure(capabilities, 'textDocument')!, 'linkedEditingRange')! linkedEditingSupport.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { let [id, options] = this.getRegistration(documentSelector, capabilities.linkedEditingRangeProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: LinkedEditingRangeRegistrationOptions): [Disposable, LinkedEditingRangeProvider] { const provider: LinkedEditingRangeProvider = { provideLinkedEditingRanges: (document, position, token) => { const client = this._client const provideLinkedEditing: ProvideLinkedEditingRangeSignature = (document, position, token) => { const params = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position) return this.sendRequest(LinkedEditingRangeRequest.type, params, token) } const middleware = client.middleware! return middleware.provideLinkedEditingRange ? middleware.provideLinkedEditingRange(document, position, token, provideLinkedEditing) : provideLinkedEditing(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerLinkedEditingRangeProvider(options.documentSelector!, provider), provider] } } ================================================ FILE: src/language-client/progress.ts ================================================ 'use strict' import type { ClientCapabilities, InitializeParams, WorkDoneProgressCreateParams } from 'vscode-languageserver-protocol' import { WorkDoneProgressCreateRequest } from '../util/protocol' import { ensure, FeatureClient, FeatureState, StaticFeature } from './features' import { ProgressPart } from './progressPart' export class ProgressFeature implements StaticFeature { private activeParts: Set = new Set() constructor(private _client: FeatureClient) { } public get method(): string { return WorkDoneProgressCreateRequest.method } public fillInitializeParams(_params: InitializeParams): void { } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(capabilities, 'window')!.workDoneProgress = true } public getState(): FeatureState { return { kind: 'window', id: WorkDoneProgressCreateRequest.method, registrations: this.activeParts.size > 0 } } public initialize(): void { let client = this._client const deleteHandler = (part: ProgressPart) => { this.activeParts.delete(part) } const createHandler = (params: WorkDoneProgressCreateParams) => { this.activeParts.add(new ProgressPart(this._client, params.token, deleteHandler)) } client.onRequest(WorkDoneProgressCreateRequest.type, createHandler) } public dispose(): void { for (const part of this.activeParts) { part.done() } this.activeParts.clear() } } ================================================ FILE: src/language-client/progressPart.ts ================================================ 'use strict' import type { Disposable, NotificationHandler, ProgressToken, ProgressType, ProtocolNotificationType, WorkDoneProgressBegin, WorkDoneProgressReport } from 'vscode-languageserver-protocol' import { disposeAll } from '../util' import { WorkDoneProgress, WorkDoneProgressCancelNotification } from '../util/protocol' import window from '../window' export interface Progress { report(value: { message?: string; increment?: number }): void } export interface ProgressContext { readonly id: string onProgress

(type: ProgressType

, token: string | number, handler: NotificationHandler

): Disposable sendNotification(type: ProtocolNotificationType, params?: P): void } export class ProgressPart { private disposables: Disposable[] = [] private _cancelled = false private _percent = 0 private _started = false private progress: Progress private _resolve: () => void private _reject: ((reason?: any) => void) | undefined public constructor(private client: ProgressContext, private token: ProgressToken, done?: (part: ProgressPart) => void) { this.disposables.push(client.onProgress(WorkDoneProgress.type, this.token, value => { switch (value.kind) { case 'begin': this.begin(value) break case 'report': this.report(value) break case 'end': this.done(value.message) if (done) { done(this) } break } })) } public begin(params: WorkDoneProgressBegin): boolean { if (this._started || this._cancelled) return false this._started = true void window.withProgress({ source: `language-client-${this.client.id}`, cancellable: params.cancellable, title: params.title, }, (progress, token) => { this.progress = progress this.report(params) if (this._cancelled) return Promise.resolve() this.disposables.push(token.onCancellationRequested(() => { this.client.sendNotification(WorkDoneProgressCancelNotification.type, { token: this.token }) this.cancel() })) return new Promise((resolve, reject) => { this._resolve = resolve this._reject = reject }) }) return true } public report(params: WorkDoneProgressReport | WorkDoneProgressBegin): void { if (this.progress) { let msg: { message?: string, increment?: number } = {} if (params.message) msg.message = params.message if (validPercent(params.percentage)) { msg.increment = Math.round(params.percentage) - this._percent this._percent = Math.round(params.percentage) } if (Object.keys(msg).length > 0) { this.progress.report(msg) } } } public cancel(): void { if (this._cancelled) return this.cleanUp() if (this._reject !== undefined) { this._reject() this._resolve = undefined this._reject = undefined } } public done(message?: string): void { if (this.progress) { let msg: { message?: string, increment?: number } = {} if (message) msg.message = message if (typeof this._percent === 'number' && this._percent > 0) msg.increment = 100 - this._percent this.progress.report(msg) } this.cleanUp() if (this._resolve) { this._resolve() this._resolve = undefined this._reject = undefined } } private cleanUp(): void { this._cancelled = true this.progress = undefined disposeAll(this.disposables) } } function validPercent(n: unknown): boolean { if (typeof n !== 'number') return false return n >= 0 && n <= 100 } ================================================ FILE: src/language-client/reference.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, Location, Position, ReferenceOptions, ReferenceRegistrationOptions, ServerCapabilities, TextDocumentRegistrationOptions } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { ProviderResult, ReferenceProvider } from '../provider' import { ReferencesRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideReferencesSignature { ( this: void, document: TextDocument, position: Position, options: { includeDeclaration: boolean }, token: CancellationToken ): ProviderResult } export interface ReferencesMiddleware { provideReferences?: ( this: void, document: TextDocument, position: Position, options: { includeDeclaration: boolean }, token: CancellationToken, next: ProvideReferencesSignature ) => ProviderResult } export class ReferencesFeature extends TextDocumentLanguageFeature< boolean | ReferenceOptions, ReferenceRegistrationOptions, ReferenceProvider, ReferencesMiddleware > { constructor(client: FeatureClient) { super(client, ReferencesRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure( ensure(capabilities, 'textDocument')!, 'references' )!.dynamicRegistration = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.referencesProvider) if (!options) { return } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: TextDocumentRegistrationOptions ): [Disposable, ReferenceProvider] { const provider: ReferenceProvider = { provideReferences: (document, position, options, token) => { const client = this._client const _providerReferences: ProvideReferencesSignature = (document, position, options, token) => { return this.sendRequest( ReferencesRequest.type, client.code2ProtocolConverter.asReferenceParams(document, position, options), token ) } const middleware = client.middleware! return middleware.provideReferences ? middleware.provideReferences(document, position, options, token, _providerReferences) : _providerReferences(document, position, options, token) } } this._client.attachExtensionName(provider) return [languages.registerReferencesProvider(options.documentSelector!, provider), provider] } } ================================================ FILE: src/language-client/rename.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, RenameOptions, RenameParams, RenameRegistrationOptions, ServerCapabilities, TextDocumentPositionParams, WorkspaceEdit } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import { Position, Range } from 'vscode-languageserver-types' import languages from '../languages' import { ProviderResult, RenameProvider } from '../provider' import * as Is from '../util/is' import { PrepareRenameRequest, PrepareSupportDefaultBehavior, RenameRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' interface DefaultBehavior { defaultBehavior: boolean } export interface PrepareRenameSignature { (this: void, document: TextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ProvideRenameEditsSignature { ( this: void, document: TextDocument, position: Position, newName: string, token: CancellationToken ): ProviderResult } export interface RenameMiddleware { prepareRename?: ( this: void, document: TextDocument, position: Position, token: CancellationToken, next: PrepareRenameSignature ) => ProviderResult provideRenameEdits?: ( this: void, document: TextDocument, position: Position, newName: string, token: CancellationToken, next: ProvideRenameEditsSignature ) => ProviderResult } export class RenameFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, RenameRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let rename = ensure(ensure(capabilities, 'textDocument')!, 'rename')! rename.dynamicRegistration = true rename.prepareSupport = true rename.honorsChangeAnnotations = true rename.prepareSupportDefaultBehavior = PrepareSupportDefaultBehavior.Identifier } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.renameProvider) if (!options) { return } if (Is.boolean(capabilities.renameProvider)) { options.prepareProvider = false } this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider(options: RenameRegistrationOptions): [Disposable, RenameProvider] { const provider: RenameProvider = { provideRenameEdits: (document, position, newName, token) => { const client = this._client const provideRenameEdits: ProvideRenameEditsSignature = (document, position, newName, token) => { const params: RenameParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), position, newName } return this.sendRequest(RenameRequest.type, params, token) } const middleware = client.middleware! return middleware.provideRenameEdits ? middleware.provideRenameEdits(document, position, newName, token, provideRenameEdits) : provideRenameEdits(document, position, newName, token) }, prepareRename: options.prepareProvider ? (document, position, token) => { const client = this._client const prepareRename: PrepareRenameSignature = (document, position, token) => { const params: TextDocumentPositionParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), position } return this.sendRequest(PrepareRenameRequest.type, params, token).then(result => { if (!result) return null if (Range.is(result)) { return result } else if (this.isDefaultBehavior(result)) { return result.defaultBehavior === true ? null : Promise.reject(new Error(`The element can't be renamed.`)) } else if (result && Range.is(result.range)) { return { range: result.range, placeholder: result.placeholder } } }) } const middleware = client.middleware! return middleware.prepareRename ? middleware.prepareRename(document, position, token, prepareRename) : prepareRename(document, position, token) } : undefined } this._client.attachExtensionName(provider) return [languages.registerRenameProvider(options.documentSelector, provider), provider] } private isDefaultBehavior(value: any): value is DefaultBehavior { const candidate: DefaultBehavior = value return candidate && Is.boolean(candidate.defaultBehavior) } } ================================================ FILE: src/language-client/selectionRange.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, Position, SelectionRange, SelectionRangeClientCapabilities, SelectionRangeOptions, SelectionRangeParams, SelectionRangeRegistrationOptions, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { ProviderResult, SelectionRangeProvider } from '../provider' import { SelectionRangeRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface ProvideSelectionRangeSignature { (this: void, document: TextDocument, positions: Position[], token: CancellationToken): ProviderResult } export interface SelectionRangeProviderMiddleware { provideSelectionRanges?: (this: void, document: TextDocument, positions: Position[], token: CancellationToken, next: ProvideSelectionRangeSignature) => ProviderResult } export class SelectionRangeFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, SelectionRangeRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities & SelectionRangeClientCapabilities): void { let capability = ensure(ensure(capabilities, 'textDocument')!, 'selectionRange')! capability.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { let [id, options] = this.getRegistration(documentSelector, capabilities.selectionRangeProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: SelectionRangeRegistrationOptions): [Disposable, SelectionRangeProvider] { const provider: SelectionRangeProvider = { provideSelectionRanges: (document, positions, token) => { const client = this._client const provideSelectionRanges: ProvideSelectionRangeSignature = (document, positions, token) => { const requestParams: SelectionRangeParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), positions } return this.sendRequest(SelectionRangeRequest.type, requestParams, token) } const middleware = client.middleware return middleware.provideSelectionRanges ? middleware.provideSelectionRanges(document, positions, token, provideSelectionRanges) : provideSelectionRanges(document, positions, token) } } this._client.attachExtensionName(provider) return [languages.registerSelectionRangeProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/semanticTokens.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, DocumentSelector, SemanticTokensDelta, SemanticTokensDeltaParams, SemanticTokensOptions, SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRegistrationOptions, ServerCapabilities } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { Range, SemanticTokenModifiers, SemanticTokens, SemanticTokenTypes } from 'vscode-languageserver-types' import languages from '../languages' import { DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, ProviderResult } from '../provider' import * as Is from '../util/is' import { Disposable, Emitter, SemanticTokensDeltaRequest, SemanticTokensRangeRequest, SemanticTokensRefreshRequest, SemanticTokensRegistrationType, SemanticTokensRequest, TokenFormat } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface DocumentSemanticsTokensSignature { (this: void, document: TextDocument, token: CancellationToken): ProviderResult } export interface DocumentSemanticsTokensEditsSignature { (this: void, document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult } export interface DocumentRangeSemanticTokensSignature { (this: void, document: TextDocument, range: Range, token: CancellationToken): ProviderResult } /** * The semantic token middleware * @since 3.16.0 */ export interface SemanticTokensMiddleware { provideDocumentSemanticTokens?: (this: void, document: TextDocument, token: CancellationToken, next: DocumentSemanticsTokensSignature) => ProviderResult provideDocumentSemanticTokensEdits?: (this: void, document: TextDocument, previousResultId: string, token: CancellationToken, next: DocumentSemanticsTokensEditsSignature) => ProviderResult provideDocumentRangeSemanticTokens?: (this: void, document: TextDocument, range: Range, token: CancellationToken, next: DocumentRangeSemanticTokensSignature) => ProviderResult } export interface SemanticTokensProviderShape { range?: DocumentRangeSemanticTokensProvider full?: DocumentSemanticTokensProvider onDidChangeSemanticTokensEmitter: Emitter } export class SemanticTokensFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, SemanticTokensRegistrationType.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const capability = ensure(ensure(capabilities, 'textDocument')!, 'semanticTokens')! capability.dynamicRegistration = true capability.tokenTypes = [ SemanticTokenTypes.namespace, SemanticTokenTypes.type, SemanticTokenTypes.class, SemanticTokenTypes.enum, SemanticTokenTypes.interface, SemanticTokenTypes.struct, SemanticTokenTypes.typeParameter, SemanticTokenTypes.parameter, SemanticTokenTypes.variable, SemanticTokenTypes.property, SemanticTokenTypes.enumMember, SemanticTokenTypes.event, SemanticTokenTypes.function, SemanticTokenTypes.method, SemanticTokenTypes.macro, SemanticTokenTypes.keyword, SemanticTokenTypes.modifier, SemanticTokenTypes.comment, SemanticTokenTypes.string, SemanticTokenTypes.number, SemanticTokenTypes.regexp, SemanticTokenTypes.decorator, SemanticTokenTypes.label, SemanticTokenTypes.operator ] capability.tokenModifiers = [ SemanticTokenModifiers.declaration, SemanticTokenModifiers.definition, SemanticTokenModifiers.readonly, SemanticTokenModifiers.static, SemanticTokenModifiers.deprecated, SemanticTokenModifiers.abstract, SemanticTokenModifiers.async, SemanticTokenModifiers.modification, SemanticTokenModifiers.documentation, SemanticTokenModifiers.defaultLibrary ] capability.formats = [TokenFormat.Relative] capability.requests = { range: true, full: { delta: true } } capability.multilineTokenSupport = false capability.overlappingTokenSupport = false capability.serverCancelSupport = true capability.augmentsSyntaxTokens = true ensure(ensure(capabilities, 'workspace')!, 'semanticTokens')!.refreshSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const client = this._client client.onRequest(SemanticTokensRefreshRequest.type, async () => { for (const provider of this.getAllProviders()) { provider.onDidChangeSemanticTokensEmitter.fire() } }) const [id, options] = this.getRegistration(documentSelector, capabilities.semanticTokensProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: SemanticTokensRegistrationOptions): [Disposable, SemanticTokensProviderShape] { const fullProvider = Is.boolean(options.full) ? options.full : options.full !== undefined const hasEditProvider = options.full !== undefined && typeof options.full !== 'boolean' && options.full.delta === true const eventEmitter: Emitter = new Emitter() const documentProvider: DocumentSemanticTokensProvider | undefined = fullProvider ? { onDidChangeSemanticTokens: eventEmitter.event, provideDocumentSemanticTokens: (document, token) => { const client = this._client const middleware = client.middleware! const provideDocumentSemanticTokens: DocumentSemanticsTokensSignature = (document, token) => { const params: SemanticTokensParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) } return this.sendRequest(SemanticTokensRequest.type, params, token) } return middleware.provideDocumentSemanticTokens ? middleware.provideDocumentSemanticTokens(document, token, provideDocumentSemanticTokens) : provideDocumentSemanticTokens(document, token) }, provideDocumentSemanticTokensEdits: hasEditProvider ? (document, previousResultId, token) => { const client = this._client const middleware = client.middleware! const provideDocumentSemanticTokensEdits: DocumentSemanticsTokensEditsSignature = (document, previousResultId, token) => { const params: SemanticTokensDeltaParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), previousResultId } return this.sendRequest(SemanticTokensDeltaRequest.type, params, token) } return middleware.provideDocumentSemanticTokensEdits ? middleware.provideDocumentSemanticTokensEdits(document, previousResultId, token, provideDocumentSemanticTokensEdits) : provideDocumentSemanticTokensEdits(document, previousResultId, token) } : undefined } : undefined const hasRangeProvider: boolean = options.range === true const rangeProvider: DocumentRangeSemanticTokensProvider | undefined = hasRangeProvider ? { provideDocumentRangeSemanticTokens: (document: TextDocument, range: Range, token: CancellationToken) => { const client = this._client const middleware = client.middleware! const provideDocumentRangeSemanticTokens: DocumentRangeSemanticTokensSignature = (document, range, token) => { const params: SemanticTokensRangeParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range } return this.sendRequest(SemanticTokensRangeRequest.type, params, token) } return middleware.provideDocumentRangeSemanticTokens ? middleware.provideDocumentRangeSemanticTokens(document, range, token, provideDocumentRangeSemanticTokens) : provideDocumentRangeSemanticTokens(document, range, token) } } : undefined const disposables: Disposable[] = [] if (documentProvider !== undefined) { this._client.attachExtensionName(documentProvider) disposables.push(languages.registerDocumentSemanticTokensProvider(options.documentSelector!, documentProvider, options.legend)) } if (rangeProvider !== undefined) { this._client.attachExtensionName(rangeProvider) disposables.push(languages.registerDocumentRangeSemanticTokensProvider(options.documentSelector!, rangeProvider, options.legend)) } return [Disposable.create(() => disposables.forEach(item => item.dispose())), { range: rangeProvider, full: documentProvider, onDidChangeSemanticTokensEmitter: eventEmitter }] } } ================================================ FILE: src/language-client/signatureHelp.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, Position, ServerCapabilities, SignatureHelp, SignatureHelpContext, SignatureHelpOptions, SignatureHelpRegistrationOptions } from 'vscode-languageserver-protocol' import { TextDocument } from "vscode-languageserver-textdocument" import languages from '../languages' import { ProviderResult, SignatureHelpProvider } from '../provider' import { SignatureHelpRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' import * as UUID from './utils/uuid' export interface ProvideSignatureHelpSignature { ( this: void, document: TextDocument, position: Position, context: SignatureHelpContext, token: CancellationToken ): ProviderResult } export interface SignatureHelpMiddleware { provideSignatureHelp?: ( this: void, document: TextDocument, position: Position, context: SignatureHelpContext, token: CancellationToken, next: ProvideSignatureHelpSignature ) => ProviderResult } export class SignatureHelpFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, SignatureHelpRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let config = ensure(ensure(capabilities, 'textDocument')!, 'signatureHelp')! config.dynamicRegistration = true config.contextSupport = true config.signatureInformation = { documentationFormat: this._client.supportedMarkupKind, activeParameterSupport: true, parameterInformation: { labelOffsetSupport: true } } } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { const options = this.getRegistrationOptions(documentSelector, capabilities.signatureHelpProvider) if (!options) return this.register({ id: UUID.generateUuid(), registerOptions: options }) } protected registerLanguageProvider( options: SignatureHelpRegistrationOptions ): [Disposable, SignatureHelpProvider] { const provider: SignatureHelpProvider = { provideSignatureHelp: (document, position, token, context) => { const client = this._client const providerSignatureHelp: ProvideSignatureHelpSignature = (document, position, context, token) => { return this.sendRequest( SignatureHelpRequest.type, client.code2ProtocolConverter.asSignatureHelpParams(document, position, context), token ) } const middleware = client.middleware! return middleware.provideSignatureHelp ? middleware.provideSignatureHelp(document, position, context, token, providerSignatureHelp) : providerSignatureHelp(document, position, context, token) } } this._client.attachExtensionName(provider) const disposable = languages.registerSignatureHelpProvider(options.documentSelector!, provider, options.triggerCharacters) return [disposable, provider] } } ================================================ FILE: src/language-client/textDocumentContent.ts ================================================ import { CancellationToken, Disposable, Emitter, StaticRegistrationOptions, TextDocumentContentRefreshRequest, TextDocumentContentRequest, type ClientCapabilities, type RegistrationType, type ServerCapabilities, type TextDocumentContentParams, type TextDocumentContentRegistrationOptions } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { ProviderResult, TextDocumentContentProvider } from '../provider' import { defaultValue, disposeAll } from '../util' import { toArray } from '../util/array' import workspace from '../workspace' import { ensure, type DynamicFeature, type FeatureClient, type FeatureState, type RegistrationData } from './features' import * as UUID from './utils/uuid' export interface ProvideTextDocumentContentSignature { (this: void, uri: URI, token: CancellationToken): ProviderResult } export interface TextDocumentContentMiddleware { provideTextDocumentContent?: (this: void, uri: URI, token: CancellationToken, next: ProvideTextDocumentContentSignature) => ProviderResult } export interface TextDocumentContentProviderShape { scheme: string onDidChangeEmitter: Emitter provider: TextDocumentContentProvider } export class TextDocumentContentFeature implements DynamicFeature { private readonly _client: FeatureClient private readonly _registrations: Map = new Map() constructor(client: FeatureClient) { this._client = client } public getState(): FeatureState { const registrations = this._registrations.size > 0 return { kind: 'workspace', id: TextDocumentContentRequest.method, registrations } } public get registrationType(): RegistrationType { return TextDocumentContentRequest.type } public getProviders(): TextDocumentContentProviderShape[] { const result: TextDocumentContentProviderShape[] = [] for (const registration of this._registrations.values()) { result.push(...registration.providers) } return result } public fillClientCapabilities(capabilities: ClientCapabilities): void { const textDocumentContent = ensure(ensure(capabilities, 'workspace')!, 'textDocumentContent')! textDocumentContent.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities): void { const client = this._client client.onRequest(TextDocumentContentRefreshRequest.type, async params => { const uri = URI.parse(params.uri) for (const registrations of this._registrations.values()) { for (const provider of registrations.providers) { if (provider.scheme === uri.scheme) { provider.onDidChangeEmitter.fire(uri) } } } }) const capability = defaultValue(defaultValue(capabilities, {}).workspace, {}).textDocumentContent if (capability) { const id = StaticRegistrationOptions.hasId(capability) ? capability.id : UUID.generateUuid() this.register({ id, registerOptions: capability }) } } public register(data: RegistrationData): void { const registrations: TextDocumentContentProviderShape[] = [] const disposables: Disposable[] = [] for (const scheme of toArray(data.registerOptions.schemes)) { const [disposable, registration] = this.registerTextDocumentContentProvider(scheme) disposables.push(disposable) registrations.push(registration) } this._registrations.set(data.id, { disposable: Disposable.create(() => { disposeAll(disposables) }), providers: registrations }) } private registerTextDocumentContentProvider(scheme: string): [Disposable, TextDocumentContentProviderShape] { const eventEmitter: Emitter = new Emitter() const provider: TextDocumentContentProvider = { onDidChange: eventEmitter.event, provideTextDocumentContent: (uri, token) => { const client = this._client const provideTextDocumentContent: ProvideTextDocumentContentSignature = (uri, token) => { const params: TextDocumentContentParams = { uri: uri.toString() } return client.sendRequest(TextDocumentContentRequest.type, params, token).then(result => { return result?.text }, error => { return client.handleFailedRequest(TextDocumentContentRequest.type, token, error, null) }) } const middleware = client.middleware return middleware.provideTextDocumentContent ? middleware.provideTextDocumentContent(uri, token, provideTextDocumentContent) : provideTextDocumentContent(uri, token) } } return [workspace.registerTextDocumentContentProvider(scheme, provider), { scheme, onDidChangeEmitter: eventEmitter, provider }] } public unregister(id: string): void { const registration = this._registrations.get(id) if (registration !== undefined) { this._registrations.delete(id) registration.disposable.dispose() } } public dispose(): void { this._registrations.forEach(value => { value.disposable.dispose() }) this._registrations.clear() } } ================================================ FILE: src/language-client/textSynchronization.ts ================================================ 'use strict' import type { ClientCapabilities, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSelector, ProtocolNotificationType, RegistrationType, SaveOptions, ServerCapabilities, TextDocumentChangeRegistrationOptions, TextDocumentRegistrationOptions, TextDocumentSaveRegistrationOptions, TextDocumentSyncOptions, TextEdit, WillSaveTextDocumentParams } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { TextDocumentWillSaveEvent } from '../core/files' import { DidChangeTextDocumentParams as TextDocumentChangeEvent } from '../types' import { defaultValue, disposeAll } from '../util' import { CancellationToken, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, DidSaveTextDocumentNotification, Disposable, Emitter, Event, TextDocumentSyncKind, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest } from '../util/protocol' import workspace from '../workspace' import { DynamicDocumentFeature, DynamicFeature, ensure, FeatureClient, NextSignature, NotificationSendEvent, NotifyingFeature, RegistrationData, TextDocumentEventFeature, TextDocumentSendFeature } from './features' import * as UUID from './utils/uuid' export interface TextDocumentSynchronizationMiddleware { didOpen?: NextSignature> didChange?: NextSignature> willSave?: NextSignature> willSaveWaitUntil?: NextSignature> didSave?: NextSignature> didClose?: NextSignature> } export interface ResolvedTextDocumentSyncCapabilities { resolvedTextDocumentSync?: TextDocumentSyncOptions } export interface DidOpenTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(textDocument: TextDocument) => Promise>, NotifyingFeature { openDocuments: Iterable } export interface DidChangeTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(event: TextDocumentChangeEvent) => Promise>, NotifyingFeature { } export interface DidSaveTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(textDocument: TextDocument) => Promise>, NotifyingFeature { } export interface DidCloseTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(textDocument: TextDocument) => Promise>, NotifyingFeature { } interface $ConfigurationOptions { textSynchronization?: { delayOpenNotifications?: boolean } } export class DidOpenTextDocumentFeature extends TextDocumentEventFeature { private readonly _syncedDocuments: Map private readonly _pendingOpenNotifications: Map private readonly _delayOpen: boolean private _pendingOpenListeners: Disposable[] | undefined constructor(client: FeatureClient, syncedDocuments: Map) { super( client, workspace.onDidOpenTextDocument, DidOpenTextDocumentNotification.type, 'didOpen', textDocument => client.code2ProtocolConverter.asOpenTextDocumentParams(textDocument), TextDocumentEventFeature.textDocumentFilter ) this._syncedDocuments = syncedDocuments this._pendingOpenNotifications = new Map() this._delayOpen = defaultValue(defaultValue(client.clientOptions.textSynchronization, {}).delayOpenNotifications, false) } public async callback(document: TextDocument): Promise { if (!this._delayOpen) { return super.callback(document) } else { if (!this.matches(document)) { return } const tabsModel = workspace.tabs if (tabsModel.isVisible(document)) { return super.callback(document) } else { this._pendingOpenNotifications.set(document.uri.toString(), document) } } } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'synchronization')!.dynamicRegistration = true } public get openDocuments(): IterableIterator { return this._syncedDocuments.values() } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { let textDocumentSyncOptions = (capabilities as ResolvedTextDocumentSyncCapabilities).resolvedTextDocumentSync if (documentSelector && textDocumentSyncOptions && textDocumentSyncOptions.openClose) { this.register({ id: UUID.generateUuid(), registerOptions: { documentSelector } }) } } public get registrationType(): RegistrationType { return DidOpenTextDocumentNotification.type } public register(data: RegistrationData): void { super.register(data) if (!data.registerOptions.documentSelector) return const onError = error => { this._client.error(`Sending document notification ${this._type.method} failed`, error) } workspace.textDocuments.forEach(textDocument => { let uri = textDocument.uri if (!this._syncedDocuments.has(uri)) { this.callback(textDocument).catch(onError) } }) if (this._delayOpen && this._pendingOpenListeners === undefined) { this._pendingOpenListeners = [] const tabsModel = workspace.tabs this._pendingOpenListeners.push(tabsModel.onClose(closed => { for (const uri of closed) { this._pendingOpenNotifications.delete(uri.toString()) } })) this._pendingOpenListeners.push(tabsModel.onOpen(opened => { for (const uri of opened) { const document = this._pendingOpenNotifications.get(uri.toString()) if (document !== undefined) { super.callback(document).catch(onError) this._pendingOpenNotifications.delete(uri.toString()) } } })) this._pendingOpenListeners.push(workspace.onDidCloseTextDocument(document => { this._pendingOpenNotifications.delete(document.uri) })) } } /** * Sends any pending open notifications unless they are for the document * being closed. * @param closingDocument The document being closed. * @returns Whether a pending open notification was dropped because it was for the closing document. */ public async sendPendingOpenNotifications(closingDocument?: string): Promise { if (!this._delayOpen) return const notifications = Array.from(this._pendingOpenNotifications.values()) this._pendingOpenNotifications.clear() let didDropOpenNotification = false for (const notification of notifications) { if (closingDocument !== undefined && notification.uri.toString() === closingDocument) { didDropOpenNotification = true continue } await super.callback(notification) } return didDropOpenNotification } protected notificationSent(textDocument: TextDocument, type: ProtocolNotificationType, params: DidOpenTextDocumentParams): void { super.notificationSent(textDocument, type, params) this._syncedDocuments.set(textDocument.uri.toString(), textDocument) } public dispose(): void { this._pendingOpenNotifications.clear() disposeAll(this._pendingOpenListeners ?? []) this._pendingOpenListeners = undefined super.dispose() } } export class DidCloseTextDocumentFeature extends TextDocumentEventFeature implements DidCloseTextDocumentFeatureShape { constructor( client: FeatureClient, private _syncedDocuments: Map ) { super( client, workspace.onDidCloseTextDocument, DidCloseTextDocumentNotification.type, 'didClose', textDocument => client.code2ProtocolConverter.asCloseTextDocumentParams(textDocument), TextDocumentEventFeature.textDocumentFilter ) } public get registrationType(): RegistrationType { return DidCloseTextDocumentNotification.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure( ensure(capabilities, 'textDocument')!, 'synchronization' )!.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { let textDocumentSyncOptions = (capabilities as ResolvedTextDocumentSyncCapabilities).resolvedTextDocumentSync if ( documentSelector && textDocumentSyncOptions && textDocumentSyncOptions.openClose ) { this.register({ id: UUID.generateUuid(), registerOptions: { documentSelector } }) } } protected notificationSent(textDocument: TextDocument, type: ProtocolNotificationType, params: DidCloseTextDocumentParams): void { super.notificationSent(textDocument, type, params) this._syncedDocuments.delete(textDocument.uri.toString()) } public unregister(id: string): void { let selector = this._selectors.get(id) if (!selector) return // The super call removed the selector from the map // of selectors. super.unregister(id) let selectors = this._selectors.values() this._syncedDocuments.forEach(textDocument => { if ( workspace.match(selector, textDocument) > 0 && !this._selectorFilter!(selectors, textDocument) ) { this.sendNotification(textDocument).catch(error => { this._client.error(`Sending document notification ${this._type.method} failed`, error) }) } }) } } interface DidChangeTextDocumentData { syncKind: 0 | 1 | 2 documentSelector: DocumentSelector } export class DidChangeTextDocumentFeature extends DynamicDocumentFeature implements DidChangeTextDocumentFeatureShape { private _listener: Disposable | undefined private readonly _changeData: Map private _onNotificationSent: Emitter> constructor(client: FeatureClient) { super(client) this._changeData = new Map() this._onNotificationSent = new Emitter() } public *getDocumentSelectors(): IterableIterator { for (const data of this._changeData.values()) { yield data.documentSelector } } public get registrationType(): RegistrationType { return DidChangeTextDocumentNotification.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'synchronization')!.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { let textDocumentSyncOptions = (capabilities as ResolvedTextDocumentSyncCapabilities).resolvedTextDocumentSync if ( documentSelector && textDocumentSyncOptions && textDocumentSyncOptions.change !== undefined && textDocumentSyncOptions.change !== TextDocumentSyncKind.None ) { this.register({ id: UUID.generateUuid(), registerOptions: Object.assign( {}, { documentSelector }, { syncKind: textDocumentSyncOptions.change } ) }) } } public register( data: RegistrationData ): void { if (!data.registerOptions.documentSelector) return if (!this._listener) { this._listener = workspace.onDidChangeTextDocument(this.callback, this) } this._changeData.set(data.id, { documentSelector: data.registerOptions.documentSelector, syncKind: data.registerOptions.syncKind }) } private callback(event: TextDocumentChangeEvent): Promise { // Text document changes are send for dirty changes as well. We don't // have dirty / undirty events in the LSP so we ignore content changes // with length zero. if (event.contentChanges.length === 0) { return } const promises: Promise[] = [] for (const changeData of this._changeData.values()) { if (workspace.match(changeData.documentSelector, event.document) > 0) { let middleware = this._client.middleware! let didChange: (event: TextDocumentChangeEvent) => Promise const client = this._client if (changeData.syncKind === TextDocumentSyncKind.Incremental) { didChange = async (event: TextDocumentChangeEvent): Promise => { const params = client.code2ProtocolConverter.asChangeTextDocumentParams(event) await this._client.sendNotification(DidChangeTextDocumentNotification.type, params) this.notificationSent(event, DidChangeTextDocumentNotification.type, params) } } else if (changeData.syncKind === TextDocumentSyncKind.Full) { didChange = async (event: TextDocumentChangeEvent): Promise => { const params = client.code2ProtocolConverter.asFullChangeTextDocumentParams(event.document) await this._client.sendNotification(DidChangeTextDocumentNotification.type, params) this.notificationSent(event, DidChangeTextDocumentNotification.type, params) } } if (didChange) { promises.push(middleware.didChange ? middleware.didChange(event, didChange) : didChange(event)) } } } return Promise.all(promises).then(undefined, error => { this._client.error(`Sending document notification ${DidChangeTextDocumentNotification.type.method} failed`, error) }) } public get onNotificationSent(): Event> { return this._onNotificationSent.event } private notificationSent(changeEvent: TextDocumentChangeEvent, type: ProtocolNotificationType, params: DidChangeTextDocumentParams): void { this._onNotificationSent.fire({ original: changeEvent, type, params }) } public unregister(id: string): void { this._changeData.delete(id) } public dispose(): void { this._changeData.clear() if (this._listener) { this._listener.dispose() this._listener = undefined } } public getProvider(document: TextDocument): { send: (event: TextDocumentChangeEvent) => Promise } | undefined { for (const changeData of this._changeData.values()) { if (workspace.match(changeData.documentSelector, document) > 0) { return { send: (event: TextDocumentChangeEvent): Promise => { return this.callback(event) } } } } return undefined } } export class WillSaveFeature extends TextDocumentEventFeature { constructor(client: FeatureClient) { super( client, workspace.onWillSaveTextDocument, WillSaveTextDocumentNotification.type, 'willSave', willSaveEvent => client.code2ProtocolConverter.asWillSaveTextDocumentParams(willSaveEvent), (selectors, willSaveEvent) => TextDocumentEventFeature.textDocumentFilter(selectors, willSaveEvent.document) ) } public get registrationType(): RegistrationType { return WillSaveTextDocumentNotification.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { let value = ensure(ensure(capabilities, 'textDocument')!, 'synchronization')! value.willSave = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { let textDocumentSyncOptions = (capabilities as ResolvedTextDocumentSyncCapabilities).resolvedTextDocumentSync if ( documentSelector && textDocumentSyncOptions && textDocumentSyncOptions.willSave ) { this.register({ id: UUID.generateUuid(), registerOptions: { documentSelector } }) } } } export class WillSaveWaitUntilFeature extends DynamicDocumentFeature { private _listener: Disposable | undefined private _selectors: Map constructor(client: FeatureClient) { super(client) this._selectors = new Map() } protected getDocumentSelectors(): IterableIterator { return this._selectors.values() } public get registrationType(): RegistrationType { return WillSaveTextDocumentWaitUntilRequest.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { let value = ensure(ensure(capabilities, 'textDocument')!, 'synchronization')! value.willSaveWaitUntil = true } public initialize( capabilities: ServerCapabilities, documentSelector: DocumentSelector ): void { let textDocumentSyncOptions = (capabilities as ResolvedTextDocumentSyncCapabilities).resolvedTextDocumentSync if ( documentSelector && documentSelector.length > 0 && textDocumentSyncOptions && textDocumentSyncOptions.willSaveWaitUntil ) { this.register({ id: UUID.generateUuid(), registerOptions: { documentSelector } }) } } public register( data: RegistrationData ): void { if (data.registerOptions.documentSelector) { if (!this._listener) { this._listener = workspace.onWillSaveTextDocument(this.callback, this) } this._selectors.set(data.id, data.registerOptions.documentSelector) } } private callback(event: TextDocumentWillSaveEvent): void { if (TextDocumentEventFeature.textDocumentFilter( this._selectors.values(), event.document)) { const client = this._client let middleware = this._client.middleware let willSaveWaitUntil = (event: TextDocumentWillSaveEvent): Thenable => { return this.sendRequest( WillSaveTextDocumentWaitUntilRequest.type, client.code2ProtocolConverter.asWillSaveTextDocumentParams(event), CancellationToken.None ) } event.waitUntil( middleware.willSaveWaitUntil ? middleware.willSaveWaitUntil(event, willSaveWaitUntil) : willSaveWaitUntil(event) ) } } public unregister(id: string): void { this._selectors.delete(id) if (this._selectors.size === 0 && this._listener) { this._listener.dispose() this._listener = undefined } } public dispose(): void { this._selectors.clear() if (this._listener) { this._listener.dispose() this._listener = undefined } } } export class DidSaveTextDocumentFeature extends TextDocumentEventFeature implements DidSaveTextDocumentFeatureShape { private _includeText: boolean constructor(client: FeatureClient) { super( client, workspace.onDidSaveTextDocument, DidSaveTextDocumentNotification.type, 'didSave', textDocument => client.code2ProtocolConverter.asSaveTextDocumentParams(textDocument, this._includeText), TextDocumentEventFeature.textDocumentFilter ) this._includeText = false } public get registrationType(): RegistrationType { return DidSaveTextDocumentNotification.type } public fillClientCapabilities(capabilities: ClientCapabilities): void { ensure(ensure(capabilities, 'textDocument')!, 'synchronization')!.didSave = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const textDocumentSyncOptions = (capabilities as ResolvedTextDocumentSyncCapabilities).resolvedTextDocumentSync if (documentSelector && textDocumentSyncOptions && textDocumentSyncOptions.save) { const saveOptions: SaveOptions = typeof textDocumentSyncOptions.save === 'boolean' ? { includeText: false } : { includeText: !!textDocumentSyncOptions.save.includeText } this.register({ id: UUID.generateUuid(), registerOptions: Object.assign({}, { documentSelector }, saveOptions) }) } } public register(data: RegistrationData): void { this._includeText = !!data.registerOptions.includeText super.register(data) } } ================================================ FILE: src/language-client/typeDefinition.ts ================================================ 'use strict' import type { ClientCapabilities, Definition, DefinitionLink, Disposable, DocumentSelector, Position, ServerCapabilities, TypeDefinitionOptions, TypeDefinitionRegistrationOptions } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { ProviderResult, TypeDefinitionProvider } from '../provider' import { CancellationToken, TypeDefinitionRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export interface ProvideTypeDefinitionSignature { ( this: void, document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } export interface TypeDefinitionMiddleware { provideTypeDefinition?: ( this: void, document: TextDocument, position: Position, token: CancellationToken, next: ProvideTypeDefinitionSignature ) => ProviderResult } export class TypeDefinitionFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, TypeDefinitionRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const typeDefinitionSupport = ensure(ensure(capabilities, 'textDocument')!, 'typeDefinition')! typeDefinitionSupport.dynamicRegistration = true typeDefinitionSupport.linkSupport = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const [id, options] = this.getRegistration(documentSelector, capabilities.typeDefinitionProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: TypeDefinitionRegistrationOptions): [Disposable, TypeDefinitionProvider] { const provider: TypeDefinitionProvider = { provideTypeDefinition: (document, position, token) => { const client = this._client const provideTypeDefinition: ProvideTypeDefinitionSignature = (document, position, token) => this.sendRequest(TypeDefinitionRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token) const middleware = client.middleware return middleware.provideTypeDefinition ? middleware.provideTypeDefinition(document, position, token, provideTypeDefinition) : provideTypeDefinition(document, position, token) } } this._client.attachExtensionName(provider) return [languages.registerTypeDefinitionProvider(options.documentSelector, provider), provider] } } ================================================ FILE: src/language-client/typeHierarchy.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, Position, ServerCapabilities, TypeHierarchyItem, TypeHierarchyOptions, TypeHierarchyRegistrationOptions } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import languages from '../languages' import { ProviderResult, TypeHierarchyProvider } from '../provider' import { TypeHierarchyPrepareRequest, TypeHierarchySubtypesRequest, TypeHierarchySupertypesRequest } from '../util/protocol' import { ensure, FeatureClient, TextDocumentLanguageFeature } from './features' export type PrepareTypeHierarchySignature = (this: void, document: TextDocument, position: Position, token: CancellationToken) => ProviderResult export type TypeHierarchySupertypesSignature = (this: void, item: TypeHierarchyItem, token: CancellationToken) => ProviderResult export type TypeHierarchySubtypesSignature = (this: void, item: TypeHierarchyItem, token: CancellationToken) => ProviderResult /** * Type hierarchy middleware * @since 3.17.0 */ export interface TypeHierarchyMiddleware { prepareTypeHierarchy?: (this: void, document: TextDocument, positions: Position, token: CancellationToken, next: PrepareTypeHierarchySignature) => ProviderResult provideTypeHierarchySupertypes?: (this: void, item: TypeHierarchyItem, token: CancellationToken, next: TypeHierarchySupertypesSignature) => ProviderResult provideTypeHierarchySubtypes?: (this: void, item: TypeHierarchyItem, token: CancellationToken, next: TypeHierarchySubtypesSignature) => ProviderResult } export class TypeHierarchyFeature extends TextDocumentLanguageFeature { constructor(client: FeatureClient) { super(client, TypeHierarchyPrepareRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { const capability = ensure(ensure(capabilities, 'textDocument')!, 'typeHierarchy')! capability.dynamicRegistration = true } public initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { const [id, options] = this.getRegistration(documentSelector, capabilities.typeHierarchyProvider) if (!id || !options) { return } this.register({ id, registerOptions: options }) } protected registerLanguageProvider(options: TypeHierarchyRegistrationOptions): [Disposable, TypeHierarchyProvider] { const client = this._client const selector = options.documentSelector! const provider = { prepareTypeHierarchy: (document: TextDocument, position: Position, token: CancellationToken): ProviderResult => { const prepareTypeHierarchy: PrepareTypeHierarchySignature = (document, position, token) => { const params = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position) return this.sendRequest(TypeHierarchyPrepareRequest.type, params, token) } const middleware = client.middleware! return middleware.prepareTypeHierarchy ? middleware.prepareTypeHierarchy(document, position, token, prepareTypeHierarchy) : prepareTypeHierarchy(document, position, token) }, provideTypeHierarchySupertypes: (item: TypeHierarchyItem, token: CancellationToken): ProviderResult => { const provideTypeHierarchySupertypes: TypeHierarchySupertypesSignature = (item, token) => { return this.sendRequest(TypeHierarchySupertypesRequest.type, { item }, token) } const middleware = client.middleware! return middleware.provideTypeHierarchySupertypes ? middleware.provideTypeHierarchySupertypes(item, token, provideTypeHierarchySupertypes) : provideTypeHierarchySupertypes(item, token) }, provideTypeHierarchySubtypes: (item: TypeHierarchyItem, token: CancellationToken): ProviderResult => { const provideTypeHierarchySubtypes: TypeHierarchySubtypesSignature = (item, token) => { return this.sendRequest(TypeHierarchySubtypesRequest.type, { item }, token) } const middleware = client.middleware! return middleware.provideTypeHierarchySubtypes ? middleware.provideTypeHierarchySubtypes(item, token, provideTypeHierarchySubtypes) : provideTypeHierarchySubtypes(item, token) } } this._client.attachExtensionName(provider) return [languages.registerTypeHierarchyProvider(selector, provider), provider] } } ================================================ FILE: src/language-client/utils/async.ts ================================================ 'use strict' import { Disposable, RAL } from '../../util/protocol' export interface ITask { (): T } export class Delayer { public defaultDelay: number private timeout: Disposable | undefined private completionPromise: Promise | undefined private onSuccess: ((value: T | Promise | undefined) => void) | undefined private task: ITask | undefined constructor(defaultDelay: number) { this.defaultDelay = defaultDelay this.timeout = undefined this.completionPromise = undefined this.onSuccess = undefined this.task = undefined } public trigger(task: ITask, delay: number = this.defaultDelay): Promise { this.task = task if (delay >= 0) { this.cancelTimeout() } if (!this.completionPromise) { this.completionPromise = new Promise(resolve => { this.onSuccess = resolve }).then(() => { this.completionPromise = undefined this.onSuccess = undefined let result = this.task!() this.task = undefined return result }) } if (delay >= 0 || this.timeout === void 0) { this.timeout = RAL().timer.setTimeout(() => { this.timeout = undefined this.onSuccess!(undefined) }, delay >= 0 ? delay : this.defaultDelay) } return this.completionPromise } public forceDelivery(): T | undefined { if (!this.completionPromise) { return undefined } this.cancelTimeout() let result: T = this.task!() this.completionPromise = undefined this.onSuccess = undefined this.task = undefined return result } public isTriggered(): boolean { return this.timeout !== undefined } public cancel(): void { this.cancelTimeout() this.completionPromise = undefined } public dispose(): void { this.cancelTimeout() } private cancelTimeout(): void { if (this.timeout !== undefined) { this.timeout.dispose() this.timeout = undefined } } } ================================================ FILE: src/language-client/utils/codeConverter.ts ================================================ import type * as protocol from 'vscode-languageserver-protocol' import { DocumentUri, TextDocument } from 'vscode-languageserver-textdocument' import { Position } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { FileCreateEvent, FileDeleteEvent, FileRenameEvent, TextDocumentWillSaveEvent } from '../../core/files' import { DidChangeTextDocumentParams as TextDocumentChangeEvent } from '../../types' import { omit } from '../../util/lodash' export interface Converter { asUri(value: URI): string asTextDocumentItem(textDocument: TextDocument): protocol.TextDocumentItem asTextDocumentIdentifier(textDocument: TextDocument): protocol.TextDocumentIdentifier asVersionedTextDocumentIdentifier(textDocument: TextDocument): protocol.VersionedTextDocumentIdentifier asOpenTextDocumentParams(textDocument: TextDocument): protocol.DidOpenTextDocumentParams asChangeTextDocumentParams(event: TextDocumentChangeEvent): protocol.DidChangeTextDocumentParams asFullChangeTextDocumentParams(textDocument: TextDocument): protocol.DidChangeTextDocumentParams asCloseTextDocumentParams(textDocument: TextDocument): protocol.DidCloseTextDocumentParams asSaveTextDocumentParams(textDocument: TextDocument, includeText?: boolean): protocol.DidSaveTextDocumentParams asWillSaveTextDocumentParams(event: TextDocumentWillSaveEvent): protocol.WillSaveTextDocumentParams asDidCreateFilesParams(event: FileCreateEvent): protocol.CreateFilesParams asDidRenameFilesParams(event: FileRenameEvent): protocol.RenameFilesParams asDidDeleteFilesParams(event: FileDeleteEvent): protocol.DeleteFilesParams asWillCreateFilesParams(event: FileCreateEvent): protocol.CreateFilesParams asWillRenameFilesParams(event: FileRenameEvent): protocol.RenameFilesParams asWillDeleteFilesParams(event: FileDeleteEvent): protocol.DeleteFilesParams asTextDocumentPositionParams(textDocument: TextDocument, position: Position): protocol.TextDocumentPositionParams asCompletionParams(textDocument: TextDocument, position: Position, context: protocol.CompletionContext): protocol.CompletionParams asSignatureHelpParams(textDocument: TextDocument, position: Position, context: protocol.SignatureHelpContext): protocol.SignatureHelpParams asReferenceParams(textDocument: TextDocument, position: Position, options: { includeDeclaration: boolean }): protocol.ReferenceParams asDocumentSymbolParams(textDocument: TextDocument): protocol.DocumentSymbolParams asCodeLensParams(textDocument: TextDocument): protocol.CodeLensParams asDocumentLinkParams(textDocument: TextDocument): protocol.DocumentLinkParams } export interface URIConverter { (value: URI): string } export function createConverter(uriConverter?: URIConverter): Converter { uriConverter = uriConverter || ((value: URI) => value.toString()) function asUri(value: URI | DocumentUri): string { if (URI.isUri(value)) { return uriConverter(value) } else { return uriConverter(URI.parse(value)) } } function asTextDocumentItem(textDocument: TextDocument): protocol.TextDocumentItem { return { uri: asUri(textDocument.uri), languageId: textDocument.languageId, version: textDocument.version, text: textDocument.getText() } } function asTextDocumentIdentifier(textDocument: TextDocument): protocol.TextDocumentIdentifier { return { uri: asUri(textDocument.uri) } } function asVersionedTextDocumentIdentifier(textDocument: TextDocument): protocol.VersionedTextDocumentIdentifier { return { uri: asUri(textDocument.uri), version: textDocument.version } } function asOpenTextDocumentParams(textDocument: TextDocument): protocol.DidOpenTextDocumentParams { return { textDocument: asTextDocumentItem(textDocument) } } function asChangeTextDocumentParams(event: TextDocumentChangeEvent): protocol.DidChangeTextDocumentParams { let { textDocument, contentChanges } = event let result: protocol.DidChangeTextDocumentParams = { textDocument: { uri: asUri(textDocument.uri), version: textDocument.version }, contentChanges: contentChanges.slice() } return result } function asFullChangeTextDocumentParams(textDocument: TextDocument): protocol.DidChangeTextDocumentParams { return { textDocument: asVersionedTextDocumentIdentifier(textDocument), contentChanges: [{ text: textDocument.getText() }] } } function asCloseTextDocumentParams(textDocument: TextDocument): protocol.DidCloseTextDocumentParams { return { textDocument: asTextDocumentIdentifier(textDocument) } } function asSaveTextDocumentParams(textDocument: TextDocument, includeText = false): protocol.DidSaveTextDocumentParams { let result: protocol.DidSaveTextDocumentParams = { textDocument: asVersionedTextDocumentIdentifier(textDocument) } if (includeText) { result.text = textDocument.getText() } return result } function asWillSaveTextDocumentParams(event: TextDocumentWillSaveEvent): protocol.WillSaveTextDocumentParams { return { textDocument: asTextDocumentIdentifier(event.document), reason: event.reason } } function asDidCreateFilesParams(event: FileCreateEvent): protocol.CreateFilesParams { return { files: event.files.map(file => ({ uri: asUri(file) })) } } function asDidRenameFilesParams(event: FileRenameEvent): protocol.RenameFilesParams { return { files: event.files.map(file => ({ oldUri: asUri(file.oldUri), newUri: asUri(file.newUri) }) ) } } function asDidDeleteFilesParams(event: FileDeleteEvent): protocol.DeleteFilesParams { return { files: event.files.map(file => ({ uri: asUri(file) })) } } function asWillCreateFilesParams(event: FileCreateEvent): protocol.CreateFilesParams { return { files: event.files.map(file => ({ uri: asUri(file) })) } } function asWillRenameFilesParams(event: FileRenameEvent): protocol.RenameFilesParams { return { files: event.files.map(file => ({ oldUri: asUri(file.oldUri), newUri: asUri(file.newUri) })) } } function asWillDeleteFilesParams(event: FileDeleteEvent): protocol.DeleteFilesParams { return { files: event.files.map(file => ({ uri: asUri(file) })) } } function asTextDocumentPositionParams(textDocument: TextDocument, position: Position): protocol.TextDocumentPositionParams { return { textDocument: asTextDocumentIdentifier(textDocument), position } } function asCompletionParams(textDocument: TextDocument, position: Position, context: protocol.CompletionContext): protocol.CompletionParams { return { textDocument: asTextDocumentIdentifier(textDocument), position, context: omit(context, ['option']) } } function asSignatureHelpParams(textDocument: TextDocument, position: Position, context: protocol.SignatureHelpContext): protocol.SignatureHelpParams { return { textDocument: asTextDocumentIdentifier(textDocument), position, context } } function asReferenceParams(textDocument: TextDocument, position: Position, options: { includeDeclaration: boolean }): protocol.ReferenceParams { return { textDocument: asTextDocumentIdentifier(textDocument), position, context: { includeDeclaration: options.includeDeclaration } } } function asDocumentSymbolParams(textDocument: TextDocument): protocol.DocumentSymbolParams { return { textDocument: asTextDocumentIdentifier(textDocument) } } function asCodeLensParams(textDocument: TextDocument): protocol.CodeLensParams { return { textDocument: asTextDocumentIdentifier(textDocument) } } function asDocumentLinkParams(textDocument: TextDocument): protocol.DocumentLinkParams { return { textDocument: asTextDocumentIdentifier(textDocument) } } return { asUri, asTextDocumentItem, asTextDocumentIdentifier, asVersionedTextDocumentIdentifier, asOpenTextDocumentParams, asChangeTextDocumentParams, asFullChangeTextDocumentParams, asCloseTextDocumentParams, asSaveTextDocumentParams, asWillSaveTextDocumentParams, asDidCreateFilesParams, asDidRenameFilesParams, asDidDeleteFilesParams, asWillCreateFilesParams, asWillRenameFilesParams, asWillDeleteFilesParams, asTextDocumentPositionParams, asCompletionParams, asSignatureHelpParams, asReferenceParams, asDocumentSymbolParams, asCodeLensParams, asDocumentLinkParams, } } ================================================ FILE: src/language-client/utils/errorHandler.ts ================================================ 'use strict' import type { InitializeError, Message, ResponseError } from 'vscode-languageserver-protocol' import { OutputChannel } from '../../types' /** * An action to be performed when the connection to a server got closed. */ export enum CloseAction { /** * Don't restart the server. The connection stays closed. */ DoNotRestart = 1, /** * Restart the server. */ Restart = 2 } export interface CloseHandlerResult { /** * The action to take. */ action: CloseAction /** * An optional message to be presented to the user. */ message?: string /** * If set to true the client assumes that the corresponding * close handler has presented an appropriate message to the * user and the message will only be log to the client's * output channel. */ handled?: boolean } /** * An action to be performed when the connection is producing errors. */ export enum ErrorAction { /** * Continue running the server. */ Continue = 1, /** * Shutdown the server. */ Shutdown = 2 } export interface ErrorHandlerResult { /** * The action to take. */ action: ErrorAction /** * An optional message to be presented to the user. */ message?: string /** * If set to true the client assumes that the corresponding * error handler has presented an appropriate message to the * user and the message will only be log to the client's * output channel. */ handled?: boolean } /** * A pluggable error handler that is invoked when the connection is either * producing errors or got closed. */ export interface ErrorHandler { /** * An error has occurred while writing or reading from the connection. * @param error - the error received * @param message - the message to be delivered to the server if know. * @param count - a count indicating how often an error is received. Will * be reset if a message got successfully send or received. */ error(error: Error, message: Message | undefined, count: number | undefined): ErrorAction | ErrorHandlerResult | Promise /** * The connection to the server got closed. */ closed(): CloseHandlerResult | Promise | CloseAction } export function toCloseHandlerResult(result: CloseHandlerResult | CloseAction): CloseHandlerResult { if (typeof result === 'number') return { action: result } return result } export interface InitializationFailedHandler { (error: ResponseError | Error | any): boolean } export class DefaultErrorHandler implements ErrorHandler { private readonly restarts: number[] public milliseconds = 3 * 60 * 1000 constructor(private name: string, private maxRestartCount: number, private outputChannel?: OutputChannel) { this.restarts = [] } public error(_error: Error, _message: Message, count: number): ErrorHandlerResult { if (count && count <= 3) { return { action: ErrorAction.Continue } } return { action: ErrorAction.Shutdown } } public closed(): CloseHandlerResult { this.restarts.push(Date.now()) if (this.restarts.length < this.maxRestartCount) { return { action: CloseAction.Restart } } else { let diff = this.restarts[this.restarts.length - 1] - this.restarts[0] if (diff <= this.milliseconds) { if (this.outputChannel) this.outputChannel.appendLine(`The server crashed ${this.maxRestartCount + 1} times in the last 3 minutes. The server will not be restarted.`) return { action: CloseAction.DoNotRestart, message: `The "${this.name}" server crashed ${this.maxRestartCount + 1} times in the last 3 minutes. The server will not be restarted.` } } else { this.restarts.shift() return { action: CloseAction.Restart } } } } } ================================================ FILE: src/language-client/utils/index.ts ================================================ import type { Disposable, MessageReader, MessageSignature, MessageWriter } from 'vscode-languageserver-protocol' import { NotificationType, NotificationType0, NotificationType1, NotificationType2, NotificationType3, NotificationType4, NotificationType5, NotificationType6, NotificationType7, NotificationType8, NotificationType9, ParameterStructures, PipeTransport, RequestType, RequestType0, RequestType1, RequestType2, RequestType3, RequestType4, RequestType5, RequestType6, RequestType7, RequestType8, RequestType9, SocketMessageReader, SocketMessageWriter, SocketTransport } from 'vscode-languageserver-protocol/node' import * as Is from '../../util/is' import { inspect, net } from '../../util/node' import { ResponseError } from '../../util/protocol' const requestTypes = [ RequestType, RequestType0, ] const notificationTypes = [ NotificationType, NotificationType0, ] export function isValidRequestType(type: any): type is string | MessageSignature { if (typeof type == 'string') return true for (let clz of requestTypes) { if (type instanceof clz) { return true } } return false } export function isValidNotificationType(type: any): type is string | MessageSignature { if (typeof type == 'string') return true for (let clz of notificationTypes) { if (type instanceof clz) { return true } } return false } export function getLocale(): string { const lang = process.env.LANG if (!lang) return 'en' return lang.split('.')[0] } export function toMethod(type: string | MessageSignature): string { return Is.string(type) ? type : type.method } export function currentTimeStamp(): string { return new Date().toLocaleTimeString() } export function getTracePrefix(data: any): string { if (data.isLSPMessage && data.type) { return `[LSP - ${currentTimeStamp()}] ` } return `[Trace - ${currentTimeStamp()}] ` } export function getParameterStructures(kind: string): ParameterStructures { switch (kind) { case 'auto': return ParameterStructures.auto case 'byPosition': return ParameterStructures.byPosition case 'byName': return ParameterStructures.byName default: return ParameterStructures.auto } } // The extension may use old version vscode-languageserver-protocol, and vscode-json-rpc checks the instanceof export function fixRequestType(type: { method: string, numberOfParams?: number } | string, params: any[]): MessageSignature | string { if (isValidRequestType(type)) return type let n = typeof type.numberOfParams === 'number' ? type.numberOfParams : params.length switch (n) { case 0: return new RequestType0(type.method) case 1: if (type['parameterStructures'] != null) { return new RequestType1(type.method, getParameterStructures(type['parameterStructures'].toString())) } return new RequestType1(type.method) case 2: return new RequestType2(type.method) case 3: return new RequestType3(type.method) case 4: return new RequestType4(type.method) case 5: return new RequestType5(type.method) case 6: return new RequestType6(type.method) case 7: return new RequestType7(type.method) case 8: return new RequestType8(type.method) case 9: return new RequestType9(type.method) default: return new RequestType(type.method) } } // The extension may use old version vscode-languageserver-protocol, and vscode-json-rpc checks the instanceof export function fixNotificationType(type: { method: string, numberOfParams?: number } | string, params: any[]): MessageSignature | string { if (isValidNotificationType(type)) return type let n = typeof type.numberOfParams === 'number' ? type.numberOfParams : params.length switch (n) { case 0: return new NotificationType0(type.method) case 1: if (type['parameterStructures'] != null) { return new NotificationType1(type.method, getParameterStructures(type['parameterStructures'].toString())) } return new NotificationType1(type.method) case 2: return new NotificationType2(type.method) case 3: return new NotificationType3(type.method) case 4: return new NotificationType4(type.method) case 5: return new NotificationType5(type.method) case 6: return new NotificationType6(type.method) case 7: return new NotificationType7(type.method) case 8: return new NotificationType8(type.method) case 9: return new NotificationType9(type.method) default: return new NotificationType(type.method) } } export function data2String(data: any, color = false): string { if (data instanceof ResponseError) { const responseError = data as ResponseError return ` Message: ${responseError.message}\n Code: ${responseError.code } ${responseError.data ? '\n' + responseError.data.toString() : ''}` } if (data instanceof Error) { if (Is.string(data.stack)) { return data.stack } return (data as Error).message } if (Is.string(data)) { return data } return inspect(data, false, null, color) } export function parseTraceData(data: any): string { if (typeof data !== 'string') return data2String(data) let prefixes = ['Params: ', 'Result: '] for (let prefix of prefixes) { if (data.startsWith(prefix)) { try { let obj = JSON.parse(data.slice(prefix.length)) return prefix + data2String(obj, true) } catch (_e) { // ignore return data } } } return data } type MessageBufferEncoding = 'ascii' | 'utf-8' export function createClientPipeTransport(pipeName: string, encoding: MessageBufferEncoding = 'utf-8'): Promise { let connectResolve: (value: [MessageReader, MessageWriter]) => void const connected = new Promise<[MessageReader, MessageWriter]>((resolve, _reject) => { connectResolve = resolve }) return new Promise((resolve, reject) => { const server = net.createServer(socket => { server.close() connectResolve([ new SocketMessageReader(socket, encoding), new SocketMessageWriter(socket, encoding) ]) }) server.on('error', reject) server.listen(pipeName, () => { server.removeListener('error', reject) resolve({ onConnected: () => { return connected }, dispose: () => { server.close() } }) }) }) } export function createClientSocketTransport(port: number, encoding: MessageBufferEncoding = 'utf-8'): Promise { let connectResolve: (value: [MessageReader, MessageWriter]) => void const connected = new Promise<[MessageReader, MessageWriter]>((resolve, _reject) => { connectResolve = resolve }) return new Promise((resolve, reject) => { const server = net.createServer(socket => { server.close() connectResolve([ new SocketMessageReader(socket, encoding), new SocketMessageWriter(socket, encoding) ]) }) server.on('error', reject) server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject) resolve({ onConnected: () => { return connected }, dispose: () => { server.close() } }) }) }) } ================================================ FILE: src/language-client/utils/logger.ts ================================================ 'use strict' import type { Logger } from 'vscode-languageserver-protocol' import { createLogger } from '../../logger' const logger = createLogger('language-client') export class ConsoleLogger implements Logger { public error(message: string): void { logger.error(message) } public warn(message: string): void { logger.warn(message) } public info(message: string): void { logger.info(message) } public log(message: string): void { logger.log(message) } } export class NullLogger implements Logger { public error(_message: string): void { } public warn(_message: string): void { } public info(_message: string): void { } public log(_message: string): void { } } ================================================ FILE: src/language-client/utils/uuid.ts ================================================ 'use strict' import { v4 as uuidv4 } from 'uuid' export function generateUuid(): string { return uuidv4() } ================================================ FILE: src/language-client/workspaceFolders.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, DidChangeWorkspaceFoldersParams, Disposable, InitializeParams, RegistrationType, ServerCapabilities, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import { defaultValue } from '../util' import { isFalsyOrEmpty } from '../util/array' import { sameFile } from '../util/fs' import { DidChangeWorkspaceFoldersNotification, WorkspaceFoldersRequest } from '../util/protocol' import workspace from '../workspace' import { DynamicFeature, FeatureClient, FeatureState, NextSignature, RegistrationData } from './features' import * as UUID from './utils/uuid' function access(target: T | undefined, key: K): T[K] | undefined { if (target === void 0) { return undefined } return target[key] } function arrayDiff(left: ReadonlyArray, right: ReadonlyArray): T[] { return left.filter(element => !right.includes(element)) } export interface WorkspaceFolderMiddleware { workspaceFolders?: WorkspaceFoldersRequest.MiddlewareSignature didChangeWorkspaceFolders?: NextSignature> } interface WorkspaceFolderWorkspaceMiddleware { workspace?: WorkspaceFolderMiddleware } export interface $WorkspaceOptions { ignoredRootPaths?: string[] } export class WorkspaceFoldersFeature implements DynamicFeature { private _listeners: Map = new Map() private _initialFolders: ReadonlyArray | undefined constructor(private _client: FeatureClient) { } public getState(): FeatureState { return { kind: 'workspace', id: this.registrationType.method, registrations: this._listeners.size > 0 } } public get registrationType(): RegistrationType { return DidChangeWorkspaceFoldersNotification.type } public getValidWorkspaceFolders(): WorkspaceFolder[] | undefined { let { workspaceFolders } = workspace let ignoredRootPaths = this._client.clientOptions.ignoredRootPaths let arr = isFalsyOrEmpty(ignoredRootPaths) ? workspaceFolders.slice(0) : workspaceFolders.filter(o => { let fsPath = URI.parse(o.uri).fsPath return ignoredRootPaths.every(p => !sameFile(p, fsPath)) }) return arr.length > 0 ? arr : undefined } public fillInitializeParams(params: InitializeParams): void { const folders = this.getValidWorkspaceFolders() this.initializeWithFolders(folders) if (folders == null) { params.workspaceFolders = null } else { params.workspaceFolders = folders.map(folder => this.asProtocol(folder)) } } protected initializeWithFolders(currentWorkspaceFolders: ReadonlyArray | undefined) { this._initialFolders = currentWorkspaceFolders } public fillClientCapabilities(capabilities: ClientCapabilities): void { capabilities.workspace = defaultValue(capabilities.workspace, {}) capabilities.workspace.workspaceFolders = true } public initialize(capabilities: ServerCapabilities): void { let client = this._client client.onRequest(WorkspaceFoldersRequest.type, (token: CancellationToken) => { let workspaceFolders: WorkspaceFoldersRequest.HandlerSignature = () => { let folders = this.getValidWorkspaceFolders() if (folders == null) { return null } return folders.map(folder => this.asProtocol(folder)) } const middleware = client.middleware.workspace return middleware && middleware.workspaceFolders ? middleware.workspaceFolders(token, workspaceFolders) : workspaceFolders(token) }) const value = access(access(access(capabilities, 'workspace'), 'workspaceFolders'), 'changeNotifications') let id: string | undefined if (typeof value === 'string') { id = value } else if (value) { id = UUID.generateUuid() } if (id) { this.register({ id, registerOptions: undefined }) } } protected sendInitialEvent(currentWorkspaceFolders: ReadonlyArray | undefined): void { let promise: Promise | undefined if (this._initialFolders && currentWorkspaceFolders) { const removed: WorkspaceFolder[] = arrayDiff(this._initialFolders, currentWorkspaceFolders) const added: WorkspaceFolder[] = arrayDiff(currentWorkspaceFolders, this._initialFolders) if (added.length > 0 || removed.length > 0) { promise = this.doSendEvent(added, removed) } } else if (this._initialFolders) { promise = this.doSendEvent([], this._initialFolders) } else if (currentWorkspaceFolders) { promise = this.doSendEvent(currentWorkspaceFolders, []) } if (promise) { promise.catch(this.onNotificationError.bind(this)) } } private onNotificationError(error: any): void { this._client.error(`Sending notification ${DidChangeWorkspaceFoldersNotification.type.method} failed`, error) } private doSendEvent(addedFolders: ReadonlyArray, removedFolders: ReadonlyArray): Promise { let params: DidChangeWorkspaceFoldersParams = { event: { added: addedFolders.map(folder => this.asProtocol(folder)), removed: removedFolders.map(folder => this.asProtocol(folder)) } } return this._client.sendNotification(DidChangeWorkspaceFoldersNotification.type, params) } public register(data: RegistrationData): void { let id = data.id let client = this._client if (this._listeners.size == 0) { let disposable = workspace.onDidChangeWorkspaceFolders(event => { let didChangeWorkspaceFolders = (e: WorkspaceFoldersChangeEvent): Promise => { return this.doSendEvent(e.added, e.removed) } let middleware = client.middleware.workspace const promise = middleware && middleware.didChangeWorkspaceFolders ? middleware.didChangeWorkspaceFolders(event, didChangeWorkspaceFolders) : didChangeWorkspaceFolders(event) if (promise) { promise.catch(this.onNotificationError.bind(this)) } }) this._listeners.set(id, disposable) let workspaceFolders = this.getValidWorkspaceFolders() this.sendInitialEvent(workspaceFolders) } } public unregister(id: string): void { const disposable = this._listeners.get(id) if (disposable === void 0) { return } this._listeners.delete(id) disposable.dispose() } public dispose(): void { for (let disposable of this._listeners.values()) { disposable.dispose() } this._listeners.clear() } private asProtocol(workspaceFolder: WorkspaceFolder): WorkspaceFolder { return { uri: this._client.code2ProtocolConverter.asUri(URI.parse(workspaceFolder.uri)), name: workspaceFolder.name } } } ================================================ FILE: src/language-client/workspaceSymbol.ts ================================================ 'use strict' import type { CancellationToken, ClientCapabilities, Disposable, DocumentSelector, RegistrationType, ServerCapabilities, SymbolInformation, WorkspaceSymbol, WorkspaceSymbolRegistrationOptions } from "vscode-languageserver-protocol" import languages from "../languages" import { ProviderResult, WorkspaceSymbolProvider } from "../provider" import { WorkspaceSymbolRequest, WorkspaceSymbolResolveRequest } from '../util/protocol' import { SupportedSymbolKinds, SupportedSymbolTags } from './documentSymbol' import { BaseFeature, DynamicFeature, ensure, FeatureClient, FeatureState, RegistrationData } from './features' import * as UUID from './utils/uuid' export interface ProvideWorkspaceSymbolsSignature { (this: void, query: string, token: CancellationToken): ProviderResult } export interface ResolveWorkspaceSymbolSignature { (this: void, item: WorkspaceSymbol, token: CancellationToken): ProviderResult } export interface WorkspaceSymbolMiddleware { provideWorkspaceSymbols?: (this: void, query: string, token: CancellationToken, next: ProvideWorkspaceSymbolsSignature) => ProviderResult resolveWorkspaceSymbol?: (this: void, item: WorkspaceSymbol, token: CancellationToken, next: ResolveWorkspaceSymbolSignature) => ProviderResult } interface WorkspaceFeatureRegistration { disposable: Disposable provider: PR } export interface WorkspaceProviderFeature { getProviders(): PR[] | undefined } abstract class WorkspaceFeature extends BaseFeature implements DynamicFeature { protected _registrations: Map> = new Map() constructor(_client: FeatureClient, private _registrationType: RegistrationType) { super(_client) } public getState(): FeatureState { const registrations = this._registrations.size > 0 return { kind: 'workspace', id: this._registrationType.method, registrations } } public get registrationType(): RegistrationType { return this._registrationType } public abstract fillClientCapabilities(capabilities: ClientCapabilities): void public abstract initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector | undefined): void public register(data: RegistrationData): void { const registration = this.registerLanguageProvider(data.registerOptions) this._registrations.set(data.id, { disposable: registration[0], provider: registration[1] }) } protected abstract registerLanguageProvider(options: RO): [Disposable, PR] public unregister(id: string): void { const registration = this._registrations.get(id) if (registration) { this._registrations.delete(id) registration.disposable.dispose() } } public dispose(): void { this._registrations.forEach(value => { value.disposable.dispose() }) this._registrations.clear() } public getProviders(): PR[] { const result: PR[] = [] for (const registration of this._registrations.values()) { result.push(registration.provider) } return result } } export class WorkspaceSymbolFeature extends WorkspaceFeature { constructor(client: FeatureClient) { super(client, WorkspaceSymbolRequest.type) } public fillClientCapabilities(capabilities: ClientCapabilities): void { let symbolCapabilities = ensure(ensure(capabilities, 'workspace')!, 'symbol')! symbolCapabilities.dynamicRegistration = true symbolCapabilities.symbolKind = { valueSet: SupportedSymbolKinds } symbolCapabilities.tagSupport = { valueSet: SupportedSymbolTags } symbolCapabilities.resolveSupport = { properties: ['location.range'] } } public initialize(capabilities: ServerCapabilities): void { if (!capabilities.workspaceSymbolProvider) { return } this.register({ id: UUID.generateUuid(), registerOptions: capabilities.workspaceSymbolProvider === true ? { workDoneProgress: false } : capabilities.workspaceSymbolProvider }) } protected registerLanguageProvider(options: WorkspaceSymbolRegistrationOptions): [Disposable, WorkspaceSymbolProvider] { const provider: WorkspaceSymbolProvider = { provideWorkspaceSymbols: (query, token) => { const client = this._client const provideWorkspaceSymbols: ProvideWorkspaceSymbolsSignature = (query, token) => { return this.sendRequest(WorkspaceSymbolRequest.type, { query }, token) as any } const middleware = client.middleware! return middleware.provideWorkspaceSymbols ? middleware.provideWorkspaceSymbols(query, token, provideWorkspaceSymbols) : provideWorkspaceSymbols(query, token) }, resolveWorkspaceSymbol: options.resolveProvider === true ? (item, token) => { const client = this._client const resolveWorkspaceSymbol: ResolveWorkspaceSymbolSignature = (item, token) => { return this.sendRequest(WorkspaceSymbolResolveRequest.type, item, token) as any } const middleware = client.middleware! return middleware.resolveWorkspaceSymbol ? middleware.resolveWorkspaceSymbol(item, token, resolveWorkspaceSymbol) : resolveWorkspaceSymbol(item, token) } : undefined } this._client.attachExtensionName(provider) return [languages.registerWorkspaceSymbolProvider(provider), provider] } } ================================================ FILE: src/languages.ts ================================================ 'use strict' import type { LinkedEditingRanges, SignatureHelpContext } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CodeAction, CodeActionContext, CodeActionKind, CodeLens, ColorInformation, ColorPresentation, DefinitionLink, DocumentHighlight, DocumentLink, DocumentSymbol, FoldingRange, FormattingOptions, Hover, InlineValue, InlineValueContext, Position, Range, SelectionRange, SemanticTokens, SemanticTokensDelta, SemanticTokensLegend, SignatureHelp, TextEdit, TypeHierarchyItem, WorkspaceEdit, WorkspaceSymbol } from 'vscode-languageserver-types' import type { Sources } from './completion/sources' import DiagnosticCollection from './diagnostic/collection' import diagnosticManager from './diagnostic/manager' import { CallHierarchyProvider, CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentLinkProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSelector, DocumentSemanticTokensProvider, DocumentSymbolProvider, DocumentSymbolProviderMetadata, FoldingContext, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionItemProvider, InlineValuesProvider, LinkedEditingRangeProvider, OnTypeFormattingEditProvider, ReferenceContext, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider, TypeHierarchyProvider, WorkspaceSymbolProvider } from './provider' import CallHierarchyManager from './provider/callHierarchyManager' import CodeActionManager from './provider/codeActionManager' import CodeLensManager from './provider/codeLensManager' import DeclarationManager from './provider/declarationManager' import DefinitionManager from './provider/definitionManager' import DocumentColorManager from './provider/documentColorManager' import DocumentHighlightManager from './provider/documentHighlightManager' import DocumentLinkManager from './provider/documentLinkManager' import DocumentSymbolManager from './provider/documentSymbolManager' import FoldingRangeManager from './provider/foldingRangeManager' import FormatManager from './provider/formatManager' import FormatRangeManager from './provider/formatRangeManager' import HoverManager from './provider/hoverManager' import ImplementationManager from './provider/implementationManager' import InlayHintManger, { InlayHintWithProvider } from './provider/inlayHintManager' import InlineCompletionItemManager, { ExtendedInlineContext } from './provider/inlineCompletionItemManager' import InlineValueManager from './provider/inlineValueManager' import LinkedEditingRangeManager from './provider/linkedEditingRangeManager' import OnTypeFormatManager from './provider/onTypeFormatManager' import ReferenceManager from './provider/referenceManager' import RenameManager from './provider/renameManager' import SelectionRangeManager from './provider/selectionRangeManager' import SemanticTokensManager from './provider/semanticTokensManager' import SemanticTokensRangeManager from './provider/semanticTokensRangeManager' import SignatureManager from './provider/signatureManager' import TypeDefinitionManager from './provider/typeDefinitionManager' import TypeHierarchyManager, { TypeHierarchyItemWithSource } from './provider/typeHierarchyManager' import WorkspaceSymbolManager from './provider/workspaceSymbolsManager' import { LocationWithTarget, TextDocumentMatch } from './types' import { disposeAll, getConditionValue } from './util' import * as Is from './util/is' import { CancellationToken, Disposable, Emitter, Event, InlineCompletionItem } from './util/protocol' import { toText } from './util/string' const eventDebounce = getConditionValue(100, 1) type withKey = { [k in K]?: Event } interface Mannger { register: (selector: DocumentSelector, provider: P, extra?: A) => Disposable } export enum ProviderName { FormatOnType = 'formatOnType', Rename = 'rename', OnTypeEdit = 'onTypeEdit', DocumentLink = 'documentLink', DocumentColor = 'documentColor', FoldingRange = 'foldingRange', Format = 'format', CodeAction = 'codeAction', FormatRange = 'formatRange', Hover = 'hover', Signature = 'signature', WorkspaceSymbols = 'workspaceSymbols', DocumentSymbol = 'documentSymbol', DocumentHighlight = 'documentHighlight', Definition = 'definition', Declaration = 'declaration', TypeDefinition = 'typeDefinition', Reference = 'reference', Implementation = 'implementation', CodeLens = 'codeLens', SelectionRange = 'selectionRange', CallHierarchy = 'callHierarchy', SemanticTokens = 'semanticTokens', SemanticTokensRange = 'semanticTokensRange', LinkedEditing = 'linkedEditing', InlayHint = 'inlayHint', InlineValue = 'inlineValue', InlineCompletion = 'inlineCompletion', TypeHierarchy = 'typeHierarchy' } class Languages { private readonly _onDidSemanticTokensRefresh = new Emitter() private readonly _onDidFoldingRangeRefresh = new Emitter() private readonly _onDidInlayHintRefresh = new Emitter() private readonly _onDidCodeLensRefresh = new Emitter() private readonly _onDidColorsRefresh = new Emitter() private readonly _onDidLinksRefresh = new Emitter() public readonly onDidSemanticTokensRefresh: Event = this._onDidSemanticTokensRefresh.event public readonly onDidFoldingRangeRefresh: Event = this._onDidFoldingRangeRefresh.event public readonly onDidInlayHintRefresh: Event = this._onDidInlayHintRefresh.event public readonly onDidCodeLensRefresh: Event = this._onDidCodeLensRefresh.event public readonly onDidColorsRefresh: Event = this._onDidColorsRefresh.event public readonly onDidLinksRefresh: Event = this._onDidLinksRefresh.event private onTypeFormatManager = new OnTypeFormatManager() private documentLinkManager = new DocumentLinkManager() private documentColorManager = new DocumentColorManager() private foldingRangeManager = new FoldingRangeManager() private renameManager = new RenameManager() private formatManager = new FormatManager() private codeActionManager = new CodeActionManager() private workspaceSymbolsManager = new WorkspaceSymbolManager() private formatRangeManager = new FormatRangeManager() private hoverManager = new HoverManager() private signatureManager = new SignatureManager() private documentSymbolManager = new DocumentSymbolManager() private documentHighlightManager = new DocumentHighlightManager() private definitionManager = new DefinitionManager() private declarationManager = new DeclarationManager() private typeDefinitionManager = new TypeDefinitionManager() private typeHierarchyManager = new TypeHierarchyManager() private referenceManager = new ReferenceManager() private implementationManager = new ImplementationManager() private codeLensManager = new CodeLensManager() private selectionRangeManager = new SelectionRangeManager() private callHierarchyManager = new CallHierarchyManager() private semanticTokensManager = new SemanticTokensManager() private semanticTokensRangeManager = new SemanticTokensRangeManager() private linkedEditingManager = new LinkedEditingRangeManager() private inlayHintManager = new InlayHintManger() public inlineCompletionItemManager = new InlineCompletionItemManager() private inlineValueManager = new InlineValueManager() public readonly registerDocumentRangeFormattingEditProvider: any public readonly registerDocumentFormattingEditProvider: any public registerReferenceProvider: (selector: DocumentSelector, provider: ReferenceProvider) => Disposable constructor() { this.registerReferenceProvider = this.registerReferencesProvider // same name as VSCode this.registerDocumentRangeFormattingEditProvider = this.registerDocumentRangeFormatProvider this.registerDocumentFormattingEditProvider = this.registerDocumentFormatProvider } public hasFormatProvider(doc: TextDocumentMatch): boolean { if (this.formatManager.hasProvider(doc)) { return true } if (this.formatRangeManager.hasProvider(doc)) { return true } return false } public registerOnTypeFormattingEditProvider( selector: DocumentSelector, provider: OnTypeFormattingEditProvider, triggerCharacters?: string[] ): Disposable { return this.onTypeFormatManager.register(selector, provider, triggerCharacters) } public registerCompletionItemProvider( name: string, shortcut: string, selector: DocumentSelector | string | null, provider: CompletionItemProvider, triggerCharacters: string[] = [], priority?: number, allCommitCharacters?: string[] ): Disposable { selector = Is.string(selector) ? [{ language: selector }] : selector let sources = require('./completion/sources').default as Sources sources.removeSource(name) return sources.createLanguageSource(name, shortcut, selector, provider, triggerCharacters, priority, allCommitCharacters) } public registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable { return this.inlineCompletionItemManager.register(selector, provider) } public registerCodeActionProvider(selector: DocumentSelector, provider: CodeActionProvider, clientId: string | undefined, codeActionKinds?: CodeActionKind[]): Disposable { return this.codeActionManager.register(selector, provider, clientId, codeActionKinds) } public registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable { return this.hoverManager.register(selector, provider) } public registerSelectionRangeProvider(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable { return this.selectionRangeManager.register(selector, provider) } public registerSignatureHelpProvider( selector: DocumentSelector, provider: SignatureHelpProvider, triggerCharacters?: string[]): Disposable { return this.signatureManager.register(selector, provider, triggerCharacters) } public registerDocumentSymbolProvider(selector: DocumentSelector, provider: DocumentSymbolProvider, metadata?: DocumentSymbolProviderMetadata): Disposable { if (metadata) provider.meta = metadata return this.documentSymbolManager.register(selector, provider) } public registerFoldingRangeProvider(selector: DocumentSelector, provider: FoldingRangeProvider): Disposable { return this.registerProviderWithEvent(selector, provider, 'onDidChangeFoldingRanges', this.foldingRangeManager, this._onDidFoldingRangeRefresh) } public registerDocumentHighlightProvider(selector: DocumentSelector, provider: DocumentHighlightProvider): Disposable { return this.documentHighlightManager.register(selector, provider) } public registerDocumentLinkProvider(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable { this._onDidLinksRefresh.fire(selector) let disposable = this.documentLinkManager.register(selector, provider) return Disposable.create(() => { disposable.dispose() this._onDidLinksRefresh.fire(selector) }) } public registerDocumentColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable { this._onDidColorsRefresh.fire(selector) let disposable = this.documentColorManager.register(selector, provider) return Disposable.create(() => { disposable.dispose() this._onDidColorsRefresh.fire(selector) }) } public registerDefinitionProvider(selector: DocumentSelector, provider: DefinitionProvider): Disposable { return this.definitionManager.register(selector, provider) } public registerDeclarationProvider(selector: DocumentSelector, provider: DeclarationProvider): Disposable { return this.declarationManager.register(selector, provider) } public registerTypeDefinitionProvider(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable { return this.typeDefinitionManager.register(selector, provider) } public registerTypeHierarchyProvider(selector: DocumentSelector, provider: TypeHierarchyProvider): Disposable { return this.typeHierarchyManager.register(selector, provider) } public registerImplementationProvider(selector: DocumentSelector, provider: ImplementationProvider): Disposable { return this.implementationManager.register(selector, provider) } public registerReferencesProvider(selector: DocumentSelector, provider: ReferenceProvider): Disposable { return this.referenceManager.register(selector, provider) } public registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable { return this.renameManager.register(selector, provider) } public registerWorkspaceSymbolProvider(provider: WorkspaceSymbolProvider): Disposable { if (arguments.length > 1 && Is.func(arguments[1].provideWorkspaceSymbols)) { provider = arguments[1] } return this.workspaceSymbolsManager.register(provider) } public registerDocumentFormatProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority = 0): Disposable { return this.formatManager.register(selector, provider, priority) } public registerDocumentRangeFormatProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, priority = 0): Disposable { return this.formatRangeManager.register(selector, provider, priority) } public registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable { return this.callHierarchyManager.register(selector, provider) } public registerCodeLensProvider(selector: DocumentSelector, provider: CodeLensProvider): Disposable { return this.registerProviderWithEvent(selector, provider, 'onDidChangeCodeLenses', this.codeLensManager, this._onDidCodeLensRefresh) } public registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable { return this.registerProviderWithEvent(selector, provider, 'onDidChangeSemanticTokens', this.semanticTokensManager, this._onDidSemanticTokensRefresh, legend) } public registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable { let disposable: Disposable | undefined let timer = setTimeout(() => { disposable = this.semanticTokensRangeManager.register(selector, provider, legend) this._onDidSemanticTokensRefresh.fire(selector) }, eventDebounce) return Disposable.create(() => { clearTimeout(timer) if (disposable) { disposable.dispose() this._onDidSemanticTokensRefresh.fire(selector) } }) } public registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable { return this.registerProviderWithEvent(selector, provider, 'onDidChangeInlayHints', this.inlayHintManager, this._onDidInlayHintRefresh) } public registerInlineValuesProvider(selector: DocumentSelector, provider: InlineValuesProvider): Disposable { // TODO onDidChangeInlineValues return this.inlineValueManager.register(selector, provider) } public registerLinkedEditingRangeProvider(selector: DocumentSelector, provider: LinkedEditingRangeProvider): Disposable { return this.linkedEditingManager.register(selector, provider) } public shouldTriggerSignatureHelp(document: TextDocument, triggerCharacter: string): boolean { return this.signatureManager.shouldTrigger(document, triggerCharacter) } public async getHover(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.hoverManager.provideHover(document, position, token) } public async getSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): Promise { return await this.signatureManager.provideSignatureHelp(document, position, token, context) } public async getDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.definitionManager.provideDefinition(document, position, token) } public async getDefinitionLinks(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.definitionManager.provideDefinitionLinks(document, position, token) } public async getDeclaration(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.declarationManager.provideDeclaration(document, position, token) } public async getTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.typeDefinitionManager.provideTypeDefinition(document, position, token) } public async getImplementation(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.implementationManager.provideImplementations(document, position, token) } public async getReferences(document: TextDocument, context: ReferenceContext, position: Position, token: CancellationToken): Promise { return await this.referenceManager.provideReferences(document, position, context, token) } public async getDocumentSymbol(document: TextDocument, token: CancellationToken): Promise { return await this.documentSymbolManager.provideDocumentSymbols(document, token) } public getDocumentSymbolMetadata(document: TextDocument): DocumentSymbolProviderMetadata | null { return this.documentSymbolManager.getMetaData(document) } public async getSelectionRanges(document: TextDocument, positions: Position[], token): Promise { return await this.selectionRangeManager.provideSelectionRanges(document, positions, token) } public async getWorkspaceSymbols(query: string, token: CancellationToken): Promise { return await this.workspaceSymbolsManager.provideWorkspaceSymbols(toText(query), token) } public async resolveWorkspaceSymbol(symbol: WorkspaceSymbol, token: CancellationToken): Promise { return await this.workspaceSymbolsManager.resolveWorkspaceSymbol(symbol, token) } public async prepareRename(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.renameManager.prepareRename(document, position, token) } public async provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): Promise { return await this.renameManager.provideRenameEdits(document, position, newName, token) } public async provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): Promise { let hasDocumentFormatter = this.formatManager.hasFormatProvider(document) if (!hasDocumentFormatter) { let hasRangeFormatter = this.formatRangeManager.hasProvider(document) if (!hasRangeFormatter) return null let end = document.positionAt(document.getText().length) let range = Range.create(Position.create(0, 0), end) return await this.provideDocumentRangeFormattingEdits(document, range, options, token) } return await this.formatManager.provideDocumentFormattingEdits(document, options, token) } public async provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): Promise { return await this.formatRangeManager.provideDocumentRangeFormattingEdits(document, range, options, token) } public async getCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise { return await this.codeActionManager.provideCodeActions(document, range, context, token) } public async getDocumentHighLight(document: TextDocument, position: Position, token: CancellationToken): Promise { return await this.documentHighlightManager.provideDocumentHighlights(document, position, token) } public async getDocumentLinks(document: TextDocument, token: CancellationToken): Promise { return await this.documentLinkManager.provideDocumentLinks(document, token) } public async resolveDocumentLink(link: DocumentLink, token: CancellationToken): Promise { return await this.documentLinkManager.resolveDocumentLink(link, token) } public async provideDocumentColors(document: TextDocument, token: CancellationToken): Promise { return await this.documentColorManager.provideDocumentColors(document, token) } public async provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): Promise { return await this.foldingRangeManager.provideFoldingRanges(document, context, token) } public async provideColorPresentations(color: ColorInformation, document: TextDocument, token: CancellationToken): Promise { return await this.documentColorManager.provideColorPresentations(color, document, token) } public async provideInlineCompletionItems(document: TextDocument, position: Position, context: ExtendedInlineContext, token: CancellationToken): Promise { return this.inlineCompletionItemManager.provideInlineCompletionItems(document, position, context, token) } public async getCodeLens(document: TextDocument, token: CancellationToken): Promise<(CodeLens | null)[]> { return await this.codeLensManager.provideCodeLenses(document, token) } public async resolveCodeLens(codeLens: CodeLens, token: CancellationToken): Promise { return await this.codeLensManager.resolveCodeLens(codeLens, token) } public async resolveCodeAction(codeAction: CodeAction, token: CancellationToken): Promise { return await this.codeActionManager.resolveCodeAction(codeAction, token) } public async provideDocumentOnTypeEdits( character: string, document: TextDocument, position: Position, token: CancellationToken ): Promise { return this.onTypeFormatManager.onCharacterType(character, document, position, token) } public canFormatOnType(character: string, document: TextDocument): boolean { return this.onTypeFormatManager.couldTrigger(document, character) != null } public async prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): Promise { return this.callHierarchyManager.prepareCallHierarchy(document, position, token) } public async provideIncomingCalls(document: TextDocument, item: CallHierarchyItem, token: CancellationToken): Promise { return this.callHierarchyManager.provideCallHierarchyIncomingCalls(document, item, token) } public async provideOutgoingCalls(document: TextDocument, item: CallHierarchyItem, token: CancellationToken): Promise { return this.callHierarchyManager.provideCallHierarchyOutgoingCalls(document, item, token) } public getLegend(document: TextDocument, range?: boolean): SemanticTokensLegend | undefined { if (range) return this.semanticTokensRangeManager.getLegend(document) return this.semanticTokensManager.getLegend(document) } public hasSemanticTokensEdits(document: TextDocument): boolean { return this.semanticTokensManager.hasSemanticTokensEdits(document) } public async provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): Promise { return this.semanticTokensManager.provideDocumentSemanticTokens(document, token) } public async provideDocumentSemanticTokensEdits(document: TextDocument, previousResultId: string, token: CancellationToken): Promise { return this.semanticTokensManager.provideDocumentSemanticTokensEdits(document, previousResultId, token) } public async provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): Promise { return this.semanticTokensRangeManager.provideDocumentRangeSemanticTokens(document, range, token) } public async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise { return this.inlayHintManager.provideInlayHints(document, range, token) } public async resolveInlayHint(hint: InlayHintWithProvider, token: CancellationToken): Promise { return this.inlayHintManager.resolveInlayHint(hint, token) } public async provideLinkedEdits(document: TextDocument, position: Position, token: CancellationToken): Promise { return this.linkedEditingManager.provideLinkedEditingRanges(document, position, token) } public async provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): Promise { return this.inlineValueManager.provideInlineValues(document, viewPort, context, token) } public async prepareTypeHierarchy(document: TextDocument, position: Position, token: CancellationToken): Promise { return this.typeHierarchyManager.prepareTypeHierarchy(document, position, token) } public async provideTypeHierarchySupertypes(item: TypeHierarchyItemWithSource, token: CancellationToken): Promise { return this.typeHierarchyManager.provideTypeHierarchySupertypes(item, token) } public async provideTypeHierarchySubtypes(item: TypeHierarchyItemWithSource, token: CancellationToken): Promise { return this.typeHierarchyManager.provideTypeHierarchySubtypes(item, token) } public createDiagnosticCollection(owner: string): DiagnosticCollection { return diagnosticManager.create(owner) } public registerProviderWithEvent, A>( selector: DocumentSelector, provider: P, key: K, manager: Mannger, emitter: Emitter, extra?: A): Disposable { let disposables: Disposable[] = [] // Wait the server finish initialize let timer = setTimeout(() => { disposables.push(manager.register(selector, provider, extra)) emitter.fire(selector) if (Is.func(provider[key])) { disposables.push(provider[key](() => { emitter.fire(selector) })) } }, eventDebounce) return Disposable.create(() => { clearTimeout(timer) let registered = disposables.length > 0 disposeAll(disposables) if (registered) emitter.fire(selector) }) } public hasProvider(id: ProviderName, document: TextDocumentMatch): boolean { switch (id) { case ProviderName.OnTypeEdit: case ProviderName.FormatOnType: return this.onTypeFormatManager.hasProvider(document) case ProviderName.Rename: return this.renameManager.hasProvider(document) case ProviderName.DocumentLink: return this.documentLinkManager.hasProvider(document) case ProviderName.DocumentColor: return this.documentColorManager.hasProvider(document) case ProviderName.FoldingRange: return this.foldingRangeManager.hasProvider(document) case ProviderName.Format: return this.formatManager.hasProvider(document) || this.formatRangeManager.hasProvider(document) case ProviderName.CodeAction: return this.codeActionManager.hasProvider(document) case ProviderName.WorkspaceSymbols: return this.workspaceSymbolsManager.hasProvider() case ProviderName.FormatRange: return this.formatRangeManager.hasProvider(document) case ProviderName.Hover: return this.hoverManager.hasProvider(document) case ProviderName.Signature: return this.signatureManager.hasProvider(document) case ProviderName.DocumentSymbol: return this.documentSymbolManager.hasProvider(document) case ProviderName.DocumentHighlight: return this.documentHighlightManager.hasProvider(document) case ProviderName.Definition: return this.definitionManager.hasProvider(document) case ProviderName.Declaration: return this.declarationManager.hasProvider(document) case ProviderName.TypeDefinition: return this.typeDefinitionManager.hasProvider(document) case ProviderName.Reference: return this.referenceManager.hasProvider(document) case ProviderName.Implementation: return this.implementationManager.hasProvider(document) case ProviderName.CodeLens: return this.codeLensManager.hasProvider(document) case ProviderName.SelectionRange: return this.selectionRangeManager.hasProvider(document) case ProviderName.CallHierarchy: return this.callHierarchyManager.hasProvider(document) case ProviderName.SemanticTokens: return this.semanticTokensManager.hasProvider(document) case ProviderName.SemanticTokensRange: return this.semanticTokensRangeManager.hasProvider(document) case ProviderName.LinkedEditing: return this.linkedEditingManager.hasProvider(document) case ProviderName.InlayHint: return this.inlayHintManager.hasProvider(document) case ProviderName.InlineCompletion: return this.inlineCompletionItemManager.hasProvider(document) case ProviderName.InlineValue: return this.inlineValueManager.hasProvider(document) case ProviderName.TypeHierarchy: return this.typeHierarchyManager.hasProvider(document) default: return false } } } export default new Languages() ================================================ FILE: src/list/basic.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Location, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { WorkspaceConfiguration } from '../configuration/types' import { ProviderResult } from '../provider' import { LocationWithTarget } from '../types' import { disposeAll } from '../util' import { lineToLocation } from '../util/fs' import { comparePosition, emptyRange } from '../util/position' import { CancellationToken, Disposable } from '../util/protocol' import { toText } from '../util/string' import workspace from '../workspace' import CommandTask, { CommandTaskOption } from './commandTask' import listConfiguration, { ListConfiguration } from './configuration' import { IList, ListAction, ListArgument, ListContext, ListItem, ListTask, LocationWithLine, MultipleListAction, SingleListAction } from './types' interface ActionOptions { persist?: boolean reload?: boolean parallel?: boolean tabPersist?: boolean } interface ArgumentItem { hasValue: boolean name: string } interface PreviewConfig { bufnr?: number winid: number position: string hlGroup: string maxHeight: number name?: string splitRight: boolean lnum: number filetype?: string range?: Range scheme?: string targetRange?: Range toplineStyle: string toplineOffset: number } export interface PreviewOptions { bufname?: string filetype?: string lines: string[] lnum?: number range?: Range sketch?: boolean } export default abstract class BasicList implements IList, Disposable { public name: string public defaultAction = 'open' public readonly actions: ListAction[] = [] public options: ListArgument[] = [] protected disposables: Disposable[] = [] protected nvim: Neovim private optionMap: Map public config: ListConfiguration constructor() { this.nvim = workspace.nvim this.config = listConfiguration } public get alignColumns(): boolean { return listConfiguration.get('alignColumns', false) } protected get floatPreview(): boolean { return listConfiguration.get('floatPreview', false) } protected get hlGroup(): string { return listConfiguration.get('previewHighlightGroup', 'Search') } protected get previewHeight(): number { return listConfiguration.get('maxPreviewHeight', 12) } protected get splitRight(): boolean { return listConfiguration.get('previewSplitRight', false) } protected get toplineStyle(): string { return listConfiguration.get('previewToplineStyle', 'offset') } protected get toplineOffset(): number { return listConfiguration.get('previewToplineOffset', 3) } public parseArguments(args: string[]): { [key: string]: string | boolean } { if (!this.optionMap) { this.optionMap = new Map() for (let opt of this.options) { let parts = opt.name.split(/,\s*/g).map(s => s.replace(/\s+.*/g, '')) let name = opt.key ? opt.key : parts[parts.length - 1].replace(/^-+/, '') for (let p of parts) { this.optionMap.set(p, { name, hasValue: opt.hasValue }) } } } let res: { [key: string]: string | boolean } = {} for (let i = 0; i < args.length; i++) { let arg = args[i] let def = this.optionMap.get(arg) if (!def) continue let value: string | boolean = true if (def.hasValue) { value = toText(args[i + 1]) i = i + 1 } res[def.name] = value } return res } /** * Get configuration of current list */ protected getConfig(): WorkspaceConfiguration { return workspace.getConfiguration(`list.source.${this.name}`) } protected addAction(name: string, fn: (item: ListItem, context: ListContext) => ProviderResult, options?: ActionOptions): void { this.createAction(Object.assign({ name, execute: fn } as any, options || {})) } protected addMultipleAction(name: string, fn: (item: ListItem[], context: ListContext) => ProviderResult, options?: ActionOptions): void { this.createAction(Object.assign({ name, multiple: true, execute: fn }, options || {})) } protected createCommandTask(opt: CommandTaskOption): CommandTask { return new CommandTask(opt) } public addLocationActions(): void { this.createAction({ name: 'preview', execute: async (item: ListItem, context: ListContext) => { let loc = await this.convertLocation(item.location) await this.previewLocation(loc, context) } }) let { nvim } = this this.createAction({ name: 'quickfix', multiple: true, execute: async (items: ListItem[]) => { let quickfixItems = await Promise.all(items.map(item => this.convertLocation(item.location).then(loc => workspace.getQuickfixItem(loc)))) await nvim.call('setqflist', [quickfixItems]) let openCommand = await nvim.getVar('coc_quickfix_open_command') as string nvim.command(typeof openCommand === 'string' ? openCommand : 'copen', true) } }) for (let name of ['open', 'tabe', 'drop', 'vsplit', 'split']) { this.createAction({ name, execute: async (item: ListItem, context: ListContext) => { await this.jumpTo(item.location, name == 'open' ? null : name, context) }, tabPersist: name === 'open' }) } } public async convertLocation(location: LocationWithTarget | LocationWithLine | string): Promise { if (typeof location == 'string') return Location.create(location, Range.create(0, 0, 0, 0)) if (Location.is(location)) return location let u = URI.parse(location.uri) if (u.scheme != 'file') return Location.create(location.uri, Range.create(0, 0, 0, 0)) return await lineToLocation(u.fsPath, location.line, location.text) } public async jumpTo(location: Location | LocationWithLine | string, command?: string, context?: ListContext): Promise { if (command == null && context && context.options.position === 'tab') { command = 'tabe' } if (typeof location == 'string') { await workspace.jumpTo(location, null, command) return } let { range, uri } = await this.convertLocation(location) let position = range.start if (position.line == 0 && position.character == 0 && comparePosition(position, range.end) == 0) { // allow plugin that remember position. position = null } await workspace.jumpTo(uri, position, command) } public createAction(action: SingleListAction | MultipleListAction): void { let { name } = action let idx = this.actions.findIndex(o => o.name == name) // allow override if (idx !== -1) this.actions.splice(idx, 1) this.actions.push(action) } protected async previewLocation(location: LocationWithTarget, context: ListContext): Promise { let { uri, range } = location let doc = workspace.getDocument(location.uri) let u = URI.parse(uri) let lines = await workspace.documentsManager.getLines(uri) let config: PreviewConfig = { bufnr: doc ? doc.bufnr : undefined, winid: context.window.id, range: emptyRange(range) ? null : range, lnum: range.start.line + 1, name: u.scheme == 'file' ? u.fsPath : uri, filetype: toVimFiletype(doc ? doc.languageId : workspace.documentsManager.getLanguageId(u.fsPath)), position: context.options.position, maxHeight: this.previewHeight, splitRight: this.splitRight, hlGroup: this.hlGroup, scheme: u.scheme, toplineStyle: this.toplineStyle, toplineOffset: this.toplineOffset, targetRange: location.targetRange } await this.openPreview(lines, config) } public async preview(options: PreviewOptions, context: ListContext): Promise { let { bufname, filetype, range, lines, lnum } = options let config: PreviewConfig = { winid: context.window.id, lnum: range ? range.start.line + 1 : lnum || 1, filetype, position: context.options.position, maxHeight: this.previewHeight, splitRight: this.splitRight, hlGroup: this.hlGroup, toplineStyle: this.toplineStyle, toplineOffset: this.toplineOffset, } if (bufname) config.name = bufname if (range) config.range = range await this.openPreview(lines, config) } private async openPreview(lines: ReadonlyArray, config: PreviewConfig): Promise { let { nvim } = this if (this.floatPreview && config.position !== 'tab') { await nvim.call('coc#list#float_preview', [lines, config]) } else { await nvim.call('coc#list#preview', [lines, config]) } nvim.command('redraw', true) } public abstract loadItems(context: ListContext, token?: CancellationToken): Promise public doHighlight(): void { // noop } public dispose(): void { disposeAll(this.disposables) } } export function toVimFiletype(filetype: string): string { switch (filetype) { case 'latex': // LaTeX (LSP language ID 'latex') has Vim filetype 'tex' return 'tex' default: return filetype } } ================================================ FILE: src/list/commandTask.ts ================================================ 'use strict' import { EventEmitter } from 'events' import { createLogger } from '../logger' import { ListItem, ListTask } from './types' import { disposeAll } from '../util' import { child_process, readline } from '../util/node' import type { Disposable } from '../util/protocol' import workspace from '../workspace' const spawn = child_process.spawn const logger = createLogger('list-commandTask') export interface CommandTaskOption { /** * Command to run. */ cmd: string /** * Arguments of command. */ args: string[] cwd?: string env?: NodeJS.ProcessEnv /** * Runs for each line, return undefined for invalid item. */ onLine: (line: string) => ListItem | undefined } export default class CommandTask extends EventEmitter implements ListTask { private disposables: Disposable[] = [] constructor(private opt: CommandTaskOption) { super() this.start() } private start(): void { let { cmd, args, cwd, onLine } = this.opt let proc = spawn(cmd, args, { cwd: cwd || workspace.cwd, windowsHide: true, shell: process.platform === 'win32' }) this.disposables.push({ dispose: () => { proc.kill() } }) proc.on('error', e => { this.emit('error', e.message) }) proc.stderr.on('data', chunk => { logger.error(`[${cmd} Error]`, chunk.toString('utf8')) }) const rl = readline.createInterface(proc.stdout) rl.on('line', line => { let res = onLine(line) if (res) this.emit('data', res) }) rl.on('close', () => { this.emit('end') }) } public dispose(): void { disposeAll(this.disposables) } } ================================================ FILE: src/list/configuration.ts ================================================ 'use strict' import window from '../window' import workspace from '../workspace' export const validKeys = [ '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '<2-LeftMouse>', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' ] export class ListConfiguration { public get debounceTime(): number { return this.get('interactiveDebounceTime', 100) } public get extendedSearchMode(): boolean { return this.get('extendedSearchMode', true) } public get smartcase(): boolean { return this.get('smartCase', false) } public get signOffset(): number { return this.get('signOffset', 900) } public get(key: string, defaultValue?: T): T { let configuration = workspace.initialConfiguration return configuration.get('list.' + key, defaultValue) } public get previousKey(): string { return this.fixKey(this.get('previousKeymap', '')) } public get nextKey(): string { return this.fixKey(this.get('nextKeymap', '')) } public fixKey(key: string): string { if (validKeys.includes(key)) return key let find = validKeys.find(s => s.toLowerCase() == key.toLowerCase()) if (find) return find void window.showErrorMessage(`Configured key "${key}" not supported.`) return null } } export default new ListConfiguration() ================================================ FILE: src/list/db.ts ================================================ /** * First byte tables length, * 4 * table_length each table byte length. */ import { fs, path } from '../util/node' import { createLogger } from '../logger' import { byteLength, byteSlice } from '../util/string' import { dataHome } from '../util/constants' const logger = createLogger('list-db') const DB_PATH = path.join(dataHome, 'list_history.dat') // text, name index, folder index type HistoryItem = [string, number, number] export class DataBase { private folders: string[] = [] private names: string[] = [] private items: HistoryItem[] = [] private _changed = false constructor() { try { this.load() } catch (e) { logger.error(`Error on load db`, e) } } public get currItems(): ReadonlyArray { return this.items } public getHistory(name: string, folder: string): string[] { let nameIndex = this.names.indexOf(name) let folderIndex = this.folders.indexOf(folder) if (nameIndex == -1 || folderIndex == -1) return [] return this.items.reduce((p, c) => { if (c[1] == nameIndex && c[2] == folderIndex) { p.push(c[0]) } return p }, [] as string[]) } public addItem(name: string, text: string, folder: string): void { let { folders, names } = this if (byteLength(text) > 255) { text = byteSlice(text, 0, 255) } if (!folders.includes(folder)) { folders.push(folder) } if (!names.includes(name)) { names.push(name) } let nameIndex = names.indexOf(name) let folderIndex = folders.indexOf(folder) let idx = this.items.findIndex(o => o[0] == text && o[1] == nameIndex && o[2] == folderIndex) if (idx != -1) this.items.splice(idx, 1) this.items.push([text, nameIndex, folderIndex]) this._changed = true } public save(): void { let { folders, items, names } = this if (!this._changed) return let bufs = folders.reduce((p, folder) => { p.push(Buffer.from(folder, 'utf8'), Buffer.alloc(1)) return p }, [] as Buffer[]) let folderBuf = Buffer.concat(bufs) bufs = names.reduce((p, name) => { p.push(Buffer.from(name, 'utf8'), Buffer.alloc(1)) return p }, [] as Buffer[]) let nameBuf = Buffer.concat(bufs) let buf = Buffer.allocUnsafe(9) buf.writeUInt8(2, 0) buf.writeUInt32BE(folderBuf.byteLength, 1) buf.writeUInt32BE(nameBuf.byteLength, 5) bufs = items.reduce((p, item) => { let b = Buffer.from(item[0], 'utf8') p.push(Buffer.from([b.byteLength]), b, Buffer.from([item[1], item[2]])) return p }, [] as Buffer[]) let resultBuf = Buffer.concat([buf, folderBuf, nameBuf, ...bufs]) fs.writeFileSync(DB_PATH, resultBuf) this._changed = false } public load(): void { if (!fs.existsSync(DB_PATH)) return let buffer = fs.readFileSync(DB_PATH) let folder_length = buffer.readUInt32BE(1) let name_length = buffer.readUInt32BE(5) let folderBuf = buffer.slice(9, 9 + folder_length) let start = 0 let folders: string[] = [] let names: string[] = [] for (let i = 0; i < folderBuf.byteLength; i++) { if (folderBuf[i] === 0) { let text = folderBuf.slice(start, i).toString('utf8') folders.push(text) start = i + 1 } } let offset = 9 + folder_length let nameBuf = buffer.slice(offset, offset + name_length) start = 0 for (let i = 0; i < nameBuf.byteLength; i++) { if (nameBuf[i] === 0) { let text = nameBuf.slice(start, i).toString('utf8') names.push(text) start = i + 1 } } let itemsBuf = buffer.slice(offset + name_length) start = 0 let total = itemsBuf.byteLength while (start < total) { let len = itemsBuf.readUInt8(start) let end = start + 1 + len let text = itemsBuf.slice(start + 1, end).toString('utf8') this.items.push([text, itemsBuf.readUInt8(end), itemsBuf.readUInt8(end + 1)]) start = end + 2 } this.names = names this.folders = folders } } export default new DataBase() ================================================ FILE: src/list/formatting.ts ================================================ 'use strict' import { ListItem } from './types' import { path } from '../util/node' import { URI } from 'vscode-uri' import { isParentFolder } from '../util/fs' import { toText } from '../util/string' export type PathFormatting = "full" | "short" | "filename" | "hidden" export interface UnformattedListItem extends Omit { label: string[] } export function fixWidth(str: string, width: number): string { if (str.length > width) { return str.slice(0, width - 1) + '.' } return str + ' '.repeat(width - str.length) } export function formatUri(uri: string, cwd: string): string { if (!uri.startsWith('file:')) return uri let filepath = URI.parse(uri).fsPath return isParentFolder(cwd, filepath) ? path.relative(cwd, filepath) : filepath } export function formatListItems(align: boolean, list: UnformattedListItem[]): ListItem[] { if (list.length === 0) { return [] } let processedList: ListItem[] = [] if (align) { const maxWidths = Array(Math.max(...list.map(item => item.label.length))).fill(0) for (let item of list) { for (let i = 0; i < item.label.length; i++) { maxWidths[i] = Math.max(maxWidths[i], (item.label[i] ?? '').length) } } processedList = list .map(item => ({ ...item, label: item.label .map((element, idx) => (element ?? '').padEnd(maxWidths[idx])) .join("\t") })) } else { processedList = list.map(item => ({ ...item, label: item.label.join("\t") })) } return processedList } export function formatPath(format: PathFormatting, pathToFormat: string): string { if (format === "hidden") { return "" } else if (format === "full") { return pathToFormat } else if (format === "short") { const segments = pathToFormat.split(path.sep) if (segments.length < 2) { return pathToFormat } const shortenedInit = segments .slice(0, segments.length - 2) .filter(seg => seg.length > 0) .map(seg => seg[0]) return [...shortenedInit, segments[segments.length - 1]].join(path.sep) } else { const segments = pathToFormat.split(path.sep) return toText(segments[segments.length - 1]) } } ================================================ FILE: src/list/history.ts ================================================ 'use strict' import { fs, path } from '../util/node' import { createLogger } from '../logger' import { isFalsyOrEmpty } from '../util/array' import { fuzzyMatch, getCharCodes } from '../util/fuzzy' import { DataBase } from './db' import { toText } from '../util/string' const logger = createLogger('list-history') export default class InputHistory { private _index = -1 private _filtered: string[] = [] private historyInput: string constructor( private prompt: { input: string }, private name: string, private db: DataBase, private cwd: string ) { } private get loaded(): string[] { return this.db.getHistory(this.name, this.cwd) } public get filtered(): ReadonlyArray { return this._filtered } public get index(): number { return this._index } public static migrate(folder: string): void { try { let files = fs.readdirSync(folder) files = files.filter(f => f.startsWith('list-') && f.endsWith('-history.json') && fs.statSync(path.join(folder, f)).isFile()) if (files.length === 0) return let db = new DataBase() for (let file of files) { let name = file.match(/^list-(.*)-history.json$/)[1] let content = fs.readFileSync(path.join(folder, file), 'utf8') let obj = JSON.parse(content) as { [key: string]: string[] } for (let [key, texts] of Object.entries(obj)) { let folder = Buffer.from(key, 'base64').toString('utf8') if (Array.isArray(texts)) { texts.forEach(text => { db.addItem(name, text, folder) }) } } } files.forEach(f => { fs.unlinkSync(path.join(folder, f)) }) db.save() } catch (e) { logger.error(`Error on migrate history:`, e) } } public get curr(): string | null { return this._index == -1 || this._filtered == null ? null : this._filtered[this._index] } public filter(): void { let { input } = this.prompt if (input === this.curr) return this.historyInput = '' if (input.length > 0) { let codes = getCharCodes(input) this._filtered = this.loaded.filter(s => fuzzyMatch(codes, s)) } else { this._filtered = this.loaded } this._index = -1 } public add(): void { let { db, prompt, cwd } = this let { input } = prompt if (!input || input.length < 2 || input == this.historyInput) return db.addItem(this.name, input, cwd) } public previous(): void { let { _filtered, _index } = this if (isFalsyOrEmpty(_filtered)) return if (_index <= 0) { this._index = _filtered.length - 1 } else { this._index = _index - 1 } this.historyInput = this.prompt.input = toText(_filtered[this._index]) } public next(): void { let { _filtered, _index } = this if (isFalsyOrEmpty(_filtered)) return if (_index == _filtered.length - 1) { this._index = 0 } else { this._index = _index + 1 } this.historyInput = this.prompt.input = toText(_filtered[this._index]) } } ================================================ FILE: src/list/manager.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../configuration/registry' import { ConfigurationScope, ConfigurationTarget } from '../configuration/types' import events from '../events' import extensions from '../extension/index' import { createLogger } from '../logger' import { defaultValue, disposeAll, getConditionValue } from '../util' import { dataHome, isVim } from '../util/constants' import { isCancellationError } from '../util/errors' import { parseExtensionName } from '../util/extensionRegistry' import { stripAnsi } from '../util/node' import { CancellationTokenSource, Disposable } from '../util/protocol' import { Registry } from '../util/registry' import { toErrorText, toInteger } from '../util/string' import window from '../window' import workspace from '../workspace' import listConfiguration from './configuration' import History from './history' import Mappings from './mappings' import Prompt from './prompt' import ListSession from './session' import CommandsList from './source/commands' import DiagnosticsList from './source/diagnostics' import ExtensionList from './source/extensions' import FolderList from './source/folders' import LinksList from './source/links' import ListsList from './source/lists' import LocationList from './source/location' import NotificationsList from './source/notifications' import OutlineList from './source/outline' import ServicesList from './source/services' import SourcesList from './source/sources' import SymbolsList from './source/symbols' import { IList, ListItem, ListOptions, ListTask, Matcher } from './types' const logger = createLogger('list-manager') const mouseKeys = ['', '', '', '<2-LeftMouse>'] const winleaveDalay = isVim ? 50 : 0 export class ListManager implements Disposable { public prompt: Prompt public mappings: Mappings private plugTs = 0 private sessionsMap: Map = new Map() private lastSession: ListSession | undefined private disposables: Disposable[] = [] private listMap: Map = new Map() constructor() { History.migrate(dataHome) } private get nvim(): Neovim { return workspace.nvim } public init(nvim: Neovim): void { this.prompt = new Prompt(nvim) this.mappings = new Mappings(this, nvim) let signText = listConfiguration.get('selectedSignText', '*') nvim.command(`sign define CocSelected text=${signText} texthl=CocSelectedText linehl=CocSelectedLine`, true) events.on('InputChar', this.onInputChar, this, this.disposables) events.on('FocusGained', async () => { let session = await this.getCurrentSession() if (session) this.prompt.drawPrompt() }, null, this.disposables) events.on('WinEnter', winid => { let session = this.getSessionByWinid(winid) if (session) this.prompt.start(session.listOptions) }, null, this.disposables) let timer: NodeJS.Timeout events.on('WinLeave', winid => { clearTimeout(timer) let session = this.getSessionByWinid(winid) if (session) { timer = setTimeout(() => { this.prompt.cancel() }, winleaveDalay) } }, null, this.disposables) workspace.onDidChangeConfiguration(e => { if (e.source !== ConfigurationTarget.Default && e.affectsConfiguration('list')) { this.mappings.createMappings() } }, null, this.disposables) this.prompt.onDidChangeInput(() => { this.session?.onInputChange() }) } public registerLists(): void { this.registerList(new LinksList(), true) this.registerList(new LocationList(), true) this.registerList(new SymbolsList(), true) this.registerList(new OutlineList(), true) this.registerList(new CommandsList(), true) this.registerList(new ExtensionList(extensions.manager), true) this.registerList(new DiagnosticsList(this), true) this.registerList(new NotificationsList(), true) this.registerList(new SourcesList(), true) this.registerList(new ServicesList(), true) this.registerList(new ListsList(this.listMap), true) this.registerList(new FolderList(), true) } public async start(args: string[]): Promise { let res = this.parseArgs(args) if (!res) return let { name } = res.list let curr = this.sessionsMap.get(name) if (curr) curr.dispose() this.prompt.start(res.options) let session = new ListSession(this.nvim, this.prompt, res.list, res.options, res.listArgs) this.sessionsMap.set(name, session) this.lastSession = session try { await session.start(args) } catch (e) { this.nvim.call('coc#prompt#stop_prompt', ['list'], true) this.nvim.command(`echo ""`, true) if (isCancellationError(e)) return void window.showErrorMessage(`Error on "CocList ${name}": ${toErrorText(e)}`) this.nvim.redrawVim() logger.error(`Error on load ${name} list:`, e) } } private getSessionByWinid(winid: number): ListSession | null { for (let session of this.sessionsMap.values()) { if (session && session.winid == winid) { this.lastSession = session return session } } return null } public async getCurrentSession(): Promise { let { id } = await this.nvim.window for (let session of this.sessionsMap.values()) { if (session && session.winid == id) { this.lastSession = session return session } } return null } public async resume(name?: string): Promise { if (!name) { await this.session?.resume() } else { let session = this.sessionsMap.get(name) if (!session) { void window.showWarningMessage(`Can't find exists ${name} list`) return } this.lastSession = session await session.resume() } } public async doAction(name?: string): Promise { let lastSession = this.lastSession if (!lastSession) return await lastSession.doAction(name) } public async first(name?: string): Promise { let s = this.getSession(name) if (s) await s.first() } public async last(name?: string): Promise { let s = this.getSession(name) if (s) await s.last() } public async previous(name?: string): Promise { let s = this.getSession(name) if (s) await s.previous() } public async next(name?: string): Promise { let s = this.getSession(name) if (s) await s.next() } public getSession(name?: string): ListSession { if (!name) return this.session return this.sessionsMap.get(name) } public async cancel(close = true): Promise { this.prompt.cancel() if (!close) return if (this.session) await this.session.hide() } /** * Clear all list sessions */ public reset(): void { this.prompt.cancel() this.lastSession = undefined for (let session of this.sessionsMap.values()) { session.dispose() } this.sessionsMap.clear() this.nvim.call('coc#prompt#stop_prompt', ['list'], true) } public async switchMatcher(): Promise { await this.session?.switchMatcher() } public async togglePreview(): Promise { let { nvim } = this let winid = await nvim.call('coc#list#get_preview', [0]) if (winid != -1) { await nvim.call('coc#list#close_preview', []) await nvim.command('redraw') } else { await this.doAction('preview') } } public async chooseAction(): Promise { let { lastSession } = this if (lastSession) await lastSession.chooseAction() } public parseArgs(args: string[]): { list: IList; options: ListOptions; listArgs: string[] } | null { let options: string[] = [] let interactive = false let autoPreview = false let numberSelect = false let noQuit = false let first = false let reverse = false let name: string let input = '' let matcher: Matcher = 'fuzzy' let position = 'bottom' let listArgs: string[] = [] let listOptions: string[] = [] let height: number | undefined for (let arg of args) { if (!name && arg.startsWith('-')) { listOptions.push(arg) } else if (!name) { if (!/^\w+$/.test(arg)) { void window.showErrorMessage(`Invalid list option: "${arg}"`) return null } name = arg } else { listArgs.push(arg) } } name = name || 'lists' let config = workspace.initialConfiguration.get(`list.source.${name}`) if (!listOptions.length && !listArgs.length) listOptions = defaultValue(config?.defaultOptions, []) if (!listArgs.length) listArgs = defaultValue(config?.defaultArgs, []) for (let opt of listOptions) { if (opt.startsWith('--input=')) { input = opt.slice(8) } else if (opt.startsWith('--height=')) { height = toInteger(opt.slice(9)) } else if (opt == '--number-select' || opt == '-N') { numberSelect = true } else if (opt == '--auto-preview' || opt == '-A') { autoPreview = true } else if (opt == '--regex' || opt == '-R') { matcher = 'regex' } else if (opt == '--strict' || opt == '-S') { matcher = 'strict' } else if (opt == '--interactive' || opt == '-I') { interactive = true } else if (opt == '--top') { position = 'top' } else if (opt == '--tab') { position = 'tab' } else if (opt == '--ignore-case' || opt == '--normal' || opt == '--no-sort') { options.push(opt.slice(2)) } else if (opt == '--first') { first = true } else if (opt == '--reverse') { reverse = true } else if (opt == '--no-quit') { noQuit = true } else { void window.showErrorMessage(`Invalid option "${opt}" of list`) return null } } let list = this.listMap.get(name) if (!list) { void window.showErrorMessage(`List ${name} not found`) return null } if (interactive && !list.interactive) { void window.showErrorMessage(`Interactive mode of "${name}" list not supported`) return null } return { list, listArgs, options: { numberSelect, autoPreview, height, reverse, noQuit, first, input, interactive, matcher, position, ignorecase: options.includes('ignore-case') ? true : false, mode: !options.includes('normal') ? 'insert' : 'normal', sort: !options.includes('no-sort') ? true : false }, } } private async onInputChar(session: string, ch: string, charmod: number): Promise { if (!ch || session != 'list') return let { mode } = this.prompt let now = Date.now() if (ch == '' || (this.plugTs && now - this.plugTs < 20)) { this.plugTs = now return } if (ch == '') { await this.cancel() return } if (mode == 'insert') { await this.onInsertInput(ch, charmod) } else { await this.onNormalInput(ch, charmod) } } public async onInsertInput(ch: string, charmod?: number): Promise { let { session } = this if (mouseKeys.includes(ch)) { await this.onMouseEvent(ch) return } if (!session) return let n = await session.doNumberSelect(ch) if (n) return let done = await this.mappings.doInsertKeymap(ch) if (done || charmod) return if (ch.startsWith('<') && ch.endsWith('>')) { await this.feedkeys(ch, false) return } for (let s of ch) { let code = s.codePointAt(0) if (code == 65533) return // exclude control character if (code < 32 || code >= 127 && code <= 159) return await this.prompt.acceptCharacter(s) } } public async onNormalInput(ch: string, _charmod?: number): Promise { if (mouseKeys.includes(ch)) { await this.onMouseEvent(ch) return } let used = await this.mappings.doNormalKeymap(ch) if (!used) await this.feedkeys(ch) } private onMouseEvent(key): Promise { return this.session?.onMouseEvent(key) } public async feedkeys(key: string, remap = true): Promise { let { nvim } = this key = key.startsWith('<') && key.endsWith('>') ? `\\${key}` : key await nvim.call('coc#prompt#stop_prompt', ['list']) await nvim.call('eval', [`feedkeys("${key}", "${remap ? 'i' : 'in'}")`]) this.triggerCursorMoved() this.prompt.start() } public async command(command: string): Promise { let { nvim } = this await nvim.call('coc#prompt#stop_prompt', ['list']) await nvim.command(command) this.triggerCursorMoved() this.prompt.start() } public async normal(command: string, bang: boolean): Promise { let { nvim } = this await nvim.call('coc#prompt#stop_prompt', ['list']) await nvim.command(`normal${bang ? '!' : ''} ${command}`) this.triggerCursorMoved() this.prompt.start() } public triggerCursorMoved(): void { if (this.nvim.isVim) this.nvim.command('doautocmd CursorMoved', true) this.nvim.call('coc#util#do_autocmd', ['CocListMoved'], true) } public async call(fname: string): Promise { if (this.session) return await this.session.call(fname) } public get session(): ListSession | undefined { return this.lastSession } public registerList(list: IList, internal = false): Disposable { let { name, interactive } = list let id: string | undefined if (!internal) id = getConditionValue(parseExtensionName(Error().stack), undefined) let removed = this.deregisterList(name) this.listMap.set(name, list) const configNode = createConfigurationNode(name, interactive, id) if (!removed) workspace.configurations.updateConfigurations([configNode]) return Disposable.create(() => { this.deregisterList(name) const configurationRegistry = Registry.as(Extensions.Configuration) configurationRegistry.deregisterConfigurations([configNode]) }) } private deregisterList(name: string): boolean { let exists = this.listMap.get(name) if (exists) { if (typeof exists.dispose == 'function') { exists.dispose() } this.listMap.delete(name) return true } return false } public get names(): string[] { return Array.from(this.listMap.keys()) } public get descriptions(): { [name: string]: string } { let d = {} for (let name of this.listMap.keys()) { let list = this.listMap.get(name) d[name] = list.description } return d } /** * Get items of {name} list * @param {string} name * @returns {Promise} */ public async loadItems(name: string): Promise { let args = [name] let res = this.parseArgs(args) if (!res || !name) return let { list, options, listArgs } = res let source = new CancellationTokenSource() let token = source.token let arr = await this.nvim.eval('[win_getid(),bufnr("%")]') let items = await list.loadItems({ options, args: listArgs, input: '', cwd: workspace.cwd, window: this.nvim.createWindow(arr[0]), buffer: this.nvim.createBuffer(arr[1]), listWindow: null }, token) if (!items || Array.isArray(items)) { return items as ListItem[] } let task = items as ListTask let newItems = await new Promise((resolve, reject) => { let items = [] task.on('data', item => { item.label = stripAnsi(item.label) items.push(item) }) task.on('end', () => { resolve(items) }) task.on('error', msg => { reject(msg instanceof Error ? msg : new Error(msg)) task.dispose() }) }) return newItems } public toggleMode(): void { let lastSession = this.lastSession if (lastSession) lastSession.toggleMode() } public get isActivated(): boolean { return this.session?.winid != null } public stop(): void { let lastSession = this.lastSession if (lastSession) lastSession.stop() } public dispose(): void { for (let session of this.sessionsMap.values()) { session.dispose() } this.sessionsMap.clear() this.lastSession = undefined disposeAll(this.disposables) } } export default new ListManager() export function createConfigurationNode(name: string, interactive: boolean, id?: string): IConfigurationNode { let properties = {} properties[`list.source.${name}.defaultAction`] = { type: 'string', default: null, description: `Default action of "${name}" list.` } properties[`list.source.${name}.defaultOptions`] = { type: 'array', default: interactive ? ['--interactive'] : [], description: `Default list options of "${name}" list, only used when both list option and argument are empty.`, uniqueItems: true, items: { type: 'string', enum: ['--top', '--normal', '--no-sort', '--input', '--height', '--tab', '--strict', '--regex', '--ignore-case', '--number-select', '--reverse', '--interactive', '--auto-preview', '--first', '--no-quit'] } } properties[`list.source.${name}.defaultArgs`] = { type: 'array', default: [], description: `Default argument list of "${name}" list, only used when list argument is empty.`, uniqueItems: true, items: { type: 'string' } } let node: IConfigurationNode = { scope: ConfigurationScope.APPLICATION, properties, } if (id) node.extensionInfo = { id } return node } ================================================ FILE: src/list/mappings.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { ListMode } from './types' import window from '../window' import listConfiguration, { validKeys } from './configuration' import { ListManager } from './manager' export default class Mappings { private insertMappings: Map void | Promise> = new Map() private normalMappings: Map void | Promise> = new Map() private userInsertMappings: Map = new Map() private userNormalMappings: Map = new Map() private actions: Map void | Promise> = new Map() constructor(private manager: ListManager, private nvim: Neovim) { let { prompt } = manager this.addAction('do:switch', async () => { await manager.switchMatcher() }) this.addAction('do:selectall', async () => { await manager.session?.ui.selectAll() }) this.addAction('do:help', async () => { await manager.session?.showHelp() }) this.addAction('do:refresh', async () => { await manager.session?.reloadItems() }) this.addAction('do:exit', async () => { await manager.cancel() }) this.addAction('do:stop', () => { manager.stop() }) this.addAction('do:cancel', async () => { await manager.cancel(false) }) this.addAction('do:toggle', async () => { await manager.session?.ui.toggleSelection() }) this.addAction('do:jumpback', () => { manager.session?.jumpBack() }) this.addAction('do:previous', async () => { await this.navigate(true) }) this.addAction('do:next', async () => { await this.navigate(false) }) this.addAction('do:defaultaction', async () => { await manager.doAction() }) this.addAction('do:chooseaction', async () => { await manager.chooseAction() }) this.addAction('do:togglemode', () => { manager.toggleMode() }) this.addAction('do:previewtoggle', async () => { await manager.togglePreview() }) this.addAction('do:previewup', () => { this.scrollPreview('up') }) this.addAction('do:previewdown', () => { this.scrollPreview('down') }) this.addAction('do:command', async () => { await manager.cancel(false) await nvim.eval('feedkeys(":")') }) this.addAction('prompt:previous', () => { manager.session?.history.previous() }) this.addAction('prompt:next', () => { manager.session?.history.next() }) this.addAction('prompt:start', () => { prompt.moveToStart() }) this.addAction('prompt:end', () => { prompt.moveToEnd() }) this.addAction('prompt:left', () => { prompt.moveLeft() }) this.addAction('prompt:right', () => { prompt.moveRight() }) this.addAction('prompt:leftword', () => { prompt.moveLeftWord() }) this.addAction('prompt:rightword', () => { prompt.moveRightWord() }) this.addAction('prompt:deleteforward', () => { prompt.onBackspace() }) this.addAction('prompt:deletebackward', () => { prompt.removeNext() }) this.addAction('prompt:removetail', () => { prompt.removeTail() }) this.addAction('prompt:removeahead', () => { prompt.removeAhead() }) this.addAction('prompt:removeword', () => { prompt.removeWord() }) this.addAction('prompt:insertregister', () => { prompt.insertRegister() }) this.addAction('prompt:paste', async () => { await prompt.paste() }) this.addAction('eval', async expr => { await prompt.eval(expr) }) this.addAction('command', async expr => { await manager.command(expr) }) this.addAction('action', async expr => { await manager.doAction(expr) }) this.addAction('feedkeys', async expr => { await manager.feedkeys(expr) }) this.addAction('feedkeys!', async expr => { await manager.feedkeys(expr, false) }) this.addAction('normal', async expr => { await manager.normal(expr, false) }) this.addAction('normal!', async expr => { await manager.normal(expr, true) }) this.addAction('call', async expr => { await manager.call(expr) }) this.addAction('expr', async expr => { let name = await manager.call(expr) await manager.doAction(name) }) this.addKeyMapping('insert', '', 'do:switch') this.addKeyMapping('insert', '', 'prompt:next') this.addKeyMapping('insert', '', 'prompt:previous') this.addKeyMapping('insert', '', 'prompt:paste') this.addKeyMapping('insert', ['', ''], 'do:defaultaction') this.addKeyMapping('insert', ['', '', '\t'], 'do:chooseaction') this.addKeyMapping('insert', '', 'do:togglemode') this.addKeyMapping('insert', '', 'do:stop') this.addKeyMapping('insert', '', 'do:refresh') this.addKeyMapping('insert', '', 'prompt:left') this.addKeyMapping('insert', '', 'prompt:right') this.addKeyMapping('insert', ['', ''], 'prompt:end') this.addKeyMapping('insert', ['', ''], 'prompt:start') this.addKeyMapping('insert', ['', '', ''], 'prompt:deleteforward') this.addKeyMapping('insert', '', 'prompt:removeword') this.addKeyMapping('insert', '', 'prompt:removeahead') this.addKeyMapping('insert', '', 'prompt:insertregister') // normal this.addKeyMapping('normal', 't', 'action:tabe') this.addKeyMapping('normal', 's', 'action:split') this.addKeyMapping('normal', 'r', 'action:refactor') this.addKeyMapping('normal', 'd', 'action:drop') this.addKeyMapping('normal', ['', '', '\r'], 'do:defaultaction') this.addKeyMapping('normal', '', 'do:selectall') this.addKeyMapping('normal', ' ', 'do:toggle') this.addKeyMapping('normal', 'p', 'do:previewtoggle') this.addKeyMapping('normal', ['', '\t', ''], 'do:chooseaction') this.addKeyMapping('normal', '', 'do:stop') this.addKeyMapping('normal', '', 'do:refresh') this.addKeyMapping('normal', '', 'do:jumpback') this.addKeyMapping('normal', '', 'do:previewdown') this.addKeyMapping('normal', '', 'do:previewup') this.addKeyMapping('normal', ['i', 'I', 'o', 'O', 'a', 'A'], 'do:togglemode') this.addKeyMapping('normal', '?', 'do:help') this.addKeyMapping('normal', ':', 'do:command') this.createMappings() } public createMappings(): void { let insertMappings = listConfiguration.get('insertMappings', {}) this.userInsertMappings = this.fixUserMappings(insertMappings, 'list.insertMappings') let normalMappings = listConfiguration.get('normalMappings', {}) this.userNormalMappings = this.fixUserMappings(normalMappings, 'list.normalMappings') } public hasUserMapping(mode: ListMode, key: string): boolean { let map = mode == 'insert' ? this.userInsertMappings : this.userNormalMappings return map.has(key) } public isValidAction(action: string): boolean { if (this.actions.has(action)) return true let [key, expr] = action.split(':', 2) if (!expr || !this.actions.has(key)) return false return true } private fixUserMappings(mappings: { [key: string]: string }, entry: string): Map { let res: Map = new Map() for (let [key, value] of Object.entries(mappings)) { if (!this.isValidAction(value)) { void window.showWarningMessage(`Invalid configuration - unable to support action "${value}" in "${entry}"`) continue } if (key.length == 1) { res.set(key, value) } else if (key.startsWith('<') && key.endsWith('>')) { if (key.toLowerCase() == '') { res.set(' ', value) } else if (key.toLowerCase() == '') { res.set('', value) } else if (validKeys.includes(key)) { res.set(key, value) } else { let find = false for (let i = 0; i < validKeys.length; i++) { if (validKeys[i].toLowerCase() == key.toLowerCase()) { find = true res.set(validKeys[i], value) break } } if (!find) void window.showWarningMessage(`Invalid configuration - unable to recognize "${key}" in "${entry}"`) } } else { void window.showWarningMessage(`Invalid configuration - unable to recognize key "${key}" in "${entry}"`) } } return res } public async navigate(up: boolean): Promise { let ui = this.manager.session?.ui if (!ui) return false await ui.moveCursor(up ? -1 : 1) return true } public async doInsertKeymap(key: string): Promise { if (key === listConfiguration.nextKey) return await this.navigate(false) if (key === listConfiguration.previousKey) return await this.navigate(true) let expr = this.userInsertMappings.get(key) if (expr) { let fn = this.getAction(expr) await Promise.resolve(fn()) return true } if (this.insertMappings.has(key)) { let fn = this.insertMappings.get(key) await Promise.resolve(fn()) return true } return false } public async doNormalKeymap(key: string): Promise { let expr = this.userNormalMappings.get(key) if (expr) { let fn = this.getAction(expr) await Promise.resolve(fn()) return true } if (this.normalMappings.has(key)) { let fn = this.normalMappings.get(key) await Promise.resolve(fn()) return true } return false } private addKeyMapping(mode: ListMode, key: string | string[], action: string): void { let mappings = mode == 'insert' ? this.insertMappings : this.normalMappings let fn = this.getAction(action) if (Array.isArray(key)) { for (let k of key) { mappings.set(k, fn) } } else { mappings.set(key, fn) } } private addAction(key: string, fn: (expr?: string) => void | Promise): void { this.actions.set(key, fn) } public getAction(action: string): () => void | Promise { if (this.actions.has(action)) return () => { return this.doAction(action) } let [key, expr] = action.split(':', 2) if (!expr || !this.actions.has(key)) throw new Error(`Invalid action ${action}`) return () => { return this.doAction(key, expr) } } public async doAction(key: string, expr?: string): Promise { let fn = this.actions.get(key) if (!fn) throw new Error(`Action ${key} doesn't exist`) await Promise.resolve(fn(expr)) } private scrollPreview(dir: 'up' | 'down'): void { const floatPreview = listConfiguration.get('floatPreview', false) let { nvim } = this nvim.pauseNotification() nvim.call('coc#list#scroll_preview', [dir, floatPreview], true) nvim.command('redraw', true) nvim.resumeNotification(false, true) } } ================================================ FILE: src/list/prompt.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Emitter, Event } from '../util/protocol' import { getUnicodeClass } from '../util/string' import listConfiguration from './configuration' import { ListMode, ListOptions, Matcher } from './types' export default class Prompt { private cursorIndex = 0 private _input = '' private _matcher: Matcher | '' private _mode: ListMode = 'insert' private interactive = false private requestInput = false private _onDidChangeInput = new Emitter() public readonly onDidChangeInput: Event = this._onDidChangeInput.event constructor(private nvim: Neovim) { } public get input(): string { return this._input } public set input(str: string) { if (this._input == str) return this.cursorIndex = str.length this._input = str this.drawPrompt() this._onDidChangeInput.fire(this._input) } public get mode(): ListMode { return this._mode } public set mode(val: ListMode) { if (val == this._mode) return this._mode = val this.drawPrompt() } public set matcher(val: Matcher) { this._matcher = val this.drawPrompt() } public start(opts?: ListOptions): void { if (opts) { this.interactive = opts.interactive this.cursorIndex = opts.input.length this._input = opts.input this._mode = opts.mode this._matcher = opts.interactive ? '' : opts.matcher } this.nvim.call('coc#prompt#start_prompt', ['list'], true) this.drawPrompt() } public cancel(): void { let { nvim } = this nvim.call('coc#prompt#stop_prompt', ['list'], true) } public reset(): void { this._input = '' this.cursorIndex = 0 } public drawPrompt(): void { let indicator = listConfiguration.get('indicator', '>') let { cursorIndex, interactive, input, _matcher } = this let cmds = ['echo ""'] if (this.mode == 'insert') { if (interactive) { cmds.push(`echohl MoreMsg | echon 'INTERACTIVE ' | echohl None`) } else if (_matcher) { cmds.push(`echohl MoreMsg | echon '${_matcher.toUpperCase()} ' | echohl None`) } cmds.push(`echohl Special | echon '${indicator} ' | echohl None`) if (cursorIndex == input.length) { cmds.push(`echon '${input.replace(/'/g, "''")}'`) cmds.push(`echohl Cursor | echon ' ' | echohl None`) } else { let pre = input.slice(0, cursorIndex) if (pre) cmds.push(`echon '${pre.replace(/'/g, "''")}'`) cmds.push(`echohl Cursor | echon '${input[cursorIndex].replace(/'/, "''")}' | echohl None`) let post = input.slice(cursorIndex + 1) cmds.push(`echon '${post.replace(/'/g, "''")}'`) } } else { cmds.push(`echohl MoreMsg | echo "" | echohl None`) } cmds.push('redraw') let cmd = cmds.join('|') this.nvim.command(cmd, true) } public moveLeft(): void { if (this.cursorIndex == 0) return this.cursorIndex = this.cursorIndex - 1 this.drawPrompt() } public moveRight(): void { if (this.cursorIndex == this._input.length) return this.cursorIndex = this.cursorIndex + 1 this.drawPrompt() } public moveLeftWord(): void { // Reuses logic from removeWord(), except that we only update the // cursorIndex and don't actually remove the word. let { cursorIndex, input } = this if (cursorIndex == 0) return let pre = input.slice(0, cursorIndex) let remain = getLastWordRemovedText(pre) this.cursorIndex = cursorIndex - (pre.length - remain.length) this.drawPrompt() this._onDidChangeInput.fire(this._input) } public moveRightWord(): void { let { cursorIndex, input } = this if (cursorIndex == input.length) return let post = input.slice(cursorIndex) let nextWord = post.match(/[\w$]+ */).at(0) ?? post this.cursorIndex = cursorIndex + nextWord.length this.drawPrompt() this._onDidChangeInput.fire(this._input) } public moveToEnd(): void { if (this.cursorIndex == this._input.length) return this.cursorIndex = this._input.length this.drawPrompt() } public moveToStart(): void { if (this.cursorIndex == 0) return this.cursorIndex = 0 this.drawPrompt() } public onBackspace(): void { let { cursorIndex, input } = this if (cursorIndex == 0) return let pre = input.slice(0, cursorIndex) let post = input.slice(cursorIndex) this.cursorIndex = cursorIndex - 1 this._input = `${pre.slice(0, pre.length - 1)}${post}` this.drawPrompt() this._onDidChangeInput.fire(this._input) } public removeNext(): void { let { cursorIndex, input } = this if (cursorIndex == input.length) return let pre = input.slice(0, cursorIndex) let post = input.slice(cursorIndex + 1) this._input = `${pre}${post}` this.drawPrompt() this._onDidChangeInput.fire(this._input) } public removeWord(): void { let { cursorIndex, input } = this if (cursorIndex == 0) return let pre = input.slice(0, cursorIndex) let post = input.slice(cursorIndex) let remain = getLastWordRemovedText(pre) this.cursorIndex = cursorIndex - (pre.length - remain.length) this._input = `${remain}${post}` this.drawPrompt() this._onDidChangeInput.fire(this._input) } public removeTail(): void { let { cursorIndex, input } = this if (cursorIndex == input.length) return let pre = input.slice(0, cursorIndex) this._input = pre this.drawPrompt() this._onDidChangeInput.fire(this._input) } public removeAhead(): void { let { cursorIndex, input } = this if (cursorIndex == 0) return let post = input.slice(cursorIndex) this.cursorIndex = 0 this._input = post this.drawPrompt() this._onDidChangeInput.fire(this._input) } public async acceptCharacter(ch: string): Promise { if (this.requestInput) { this.requestInput = false if (/^[0-9a-z"%#*+/:\-.]$/.test(ch)) { let text = await this.nvim.call('getreg', ch) as string text = text.replace(/\n/g, ' ') this.addText(text) } } else { this.addText(ch) } } public insertRegister(): void { this.requestInput = true } public async paste(): Promise { let text = await this.nvim.eval('@*') as string text = text.replace(/\n/g, '') if (!text) return this.addText(text) } public async eval(expression: string): Promise { let text = await this.nvim.call('eval', [expression]) as string text = text.replace(/\n/g, '') this.addText(text) } private addText(text: string): void { let { cursorIndex, input } = this this.cursorIndex = cursorIndex + text.length let pre = input.slice(0, cursorIndex) let post = input.slice(cursorIndex) this._input = `${pre}${text}${post}` this.drawPrompt() this._onDidChangeInput.fire(this._input) } } function getLastWordRemovedText(text: string): string { let res = text // Remove last whitespaces res = res.trimEnd() if (res === "") return res // Remove last contiguous characters of the same unicode class. const last = getUnicodeClass(res[res.length - 1]) while (res !== "" && getUnicodeClass(res[res.length - 1]) === last) { res = res.slice(0, res.length - 1) } return res } ================================================ FILE: src/list/session.ts ================================================ 'use strict' import type { Buffer, Neovim, Window } from '@chemzqm/neovim' import Highlighter from '../model/highlighter' import { defaultValue, disposeAll, getConditionValue, wait } from '../util' import { debounce } from '../util/node' import { Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import listConfiguration from './configuration' import db from './db' import InputHistory from './history' import Prompt from './prompt' import { IList, ListAction, ListContext, ListItem, ListMode, ListOptions, Matcher } from './types' import UI from './ui' import Worker from './worker' const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] const debounceTime = getConditionValue(50, 1) /** * Activated list session with UI and worker */ export default class ListSession { public readonly history: InputHistory public readonly ui: UI public readonly worker: Worker private cwd: string private loadingFrame = '' private timer: NodeJS.Timeout private hidden = false private disposables: Disposable[] = [] private savedHeight: number private targetWinid: number | undefined private targetBufnr: number | undefined /** * Original list arguments. */ private args: string[] = [] constructor( private nvim: Neovim, private prompt: Prompt, private list: IList, public readonly listOptions: ListOptions, private listArgs: string[] ) { this.ui = new UI(nvim, list.name, listOptions) this.history = new InputHistory(prompt, list.name, db, workspace.cwd) this.worker = new Worker(list, prompt, listOptions) let debouncedChangeLine = debounce(async () => { let [previewing, currwin, lnum] = await nvim.eval('[coc#list#has_preview(),win_getid(),line(".")]') as [number, number, number] if (previewing && currwin == this.winid) { let idx = this.ui.lnumToIndex(lnum) await this.doPreview(idx) } }, debounceTime) this.disposables.push({ dispose: () => { debouncedChangeLine.clear() } }) this.ui.onDidChangeLine(debouncedChangeLine, null, this.disposables) this.ui.onDidChangeLine(this.resolveItem, this, this.disposables) this.ui.onDidLineChange(this.resolveItem, this, this.disposables) let debounced = debounce(async () => { this.updateStatus() let { autoPreview } = this.listOptions if (!autoPreview) { let [previewing, mode] = await nvim.eval('[coc#list#has_preview(),mode()]') as [number, string] if (mode != 'n' || !previewing) return } await this.doAction('preview') }, 50) this.disposables.push({ dispose: () => { debounced.clear() } }) this.ui.onDidLineChange(debounced, null, this.disposables) this.ui.onDidOpen(async () => { if (typeof this.list.doHighlight == 'function') { this.list.doHighlight() } if (this.listOptions.first) { await this.doAction() } }, null, this.disposables) this.ui.onDidClose(this.hide as any, this, this.disposables) this.ui.onDidDoubleClick(this.doAction as any, this, this.disposables) this.worker.onDidChangeItems(ev => { if (this.hidden) return this.ui.onDidChangeItems(ev) }, null, this.disposables) let start = 0 let timer: NodeJS.Timeout let interval: NodeJS.Timeout this.disposables.push(Disposable.create(() => { clearTimeout(timer) clearInterval(interval) })) this.worker.onDidChangeLoading(loading => { if (this.hidden) return if (timer) clearTimeout(timer) if (loading) { start = Date.now() if (interval) clearInterval(interval) interval = setInterval(() => { let idx = Math.floor((Date.now() - start) % 1000 / 100) this.loadingFrame = frames[idx] this.updateStatus() }, 100) } else { timer = setTimeout(() => { this.loadingFrame = '' if (interval) clearInterval(interval) interval = null this.updateStatus() }, Math.max(0, 200 - (Date.now() - start))) } }, null, this.disposables) } public async start(args: string[]): Promise { this.args = args this.cwd = workspace.cwd this.hidden = false let { listArgs } = this let res = await this.nvim.eval(`[win_getid(),bufnr("%"),${workspace.isVim ? 'winheight("%")' : 'nvim_win_get_height(0)'}]`) this.listArgs = listArgs this.history.filter() this.targetWinid = res[0] this.targetBufnr = res[1] this.savedHeight = res[2] await this.worker.loadItems(this.context) } public async reloadItems(): Promise { if (!this.ui.winid) return await this.worker.loadItems(this.context, true) } public async call(fname: string): Promise { await this.nvim.call('coc#prompt#stop_prompt', ['list']) let targets = await this.ui.getItems() let context = { name: this.name, args: this.listArgs, input: this.prompt.input, winid: this.targetWinid, bufnr: this.targetBufnr, targets } let res = await this.nvim.call(fname, [context]) this.prompt.start() return res } public async chooseAction(): Promise { let { nvim, defaultAction } = this let { actions } = this.list let names: string[] = actions.map(o => o.name) let idx = names.indexOf(defaultAction.name) if (idx != -1) { names.splice(idx, 1) names.unshift(defaultAction.name) } let shortcuts: Set = new Set() let choices: string[] = [] let invalids: string[] = [] let menuAction = workspace.env.dialog && listConfiguration.get('menuAction', false) for (let name of names) { let i = 0 for (let ch of name) { if (!shortcuts.has(ch)) { shortcuts.add(ch) choices.push(`${name.slice(0, i)}&${name.slice(i)}`) break } i++ } if (i == name.length) { invalids.push(name) } } if (invalids.length && !menuAction) { names = names.filter(s => !invalids.includes(s)) } let n: number if (menuAction) { nvim.call('coc#prompt#stop_prompt', ['list'], true) n = await window.showMenuPicker(names, { title: 'Choose action', shortcuts: true }) n = n + 1 this.prompt.start() } else { await nvim.call('coc#prompt#stop_prompt', ['list']) n = await nvim.call('confirm', ['Choose action:', choices.join('\n')]) as number await wait(10) this.prompt.start() } if (n) await this.doAction(names[n - 1]) } public async doAction(name?: string): Promise { let { list } = this let action: ListAction if (name != null) { action = list.actions.find(o => o.name == name) if (!action) { void window.showErrorMessage(`Action ${name} not found`) return } } else { action = this.defaultAction } let items: ListItem[] if (name == 'preview') { let item = await this.ui.item items = item ? [item] : [] } else { items = await this.ui.getItems() } if (items.length) await this.doItemAction(items, action) } public async doPreview(index: number): Promise { let item = this.ui.getItem(index) let action = this.list.actions.find(o => o.name == 'preview') if (!item || !action) return await this.doItemAction([item], action) } public async first(): Promise { await this.doDefaultAction(0) } public async last(): Promise { await this.doDefaultAction(this.ui.length - 1) } public async previous(): Promise { await this.doDefaultAction(this.ui.index - 1) } public async next(): Promise { await this.doDefaultAction(this.ui.index + 1) } private async doDefaultAction(index: number): Promise { let { ui } = this let item = ui.getItem(index) if (!item) return await this.ui.setIndex(index) await this.doItemAction([item], this.defaultAction) await ui.echoMessage(item) } /** * list name */ public get name(): string { return this.list.name } /** * Window id used by list. * @returns {number | undefined} */ public get winid(): number | undefined { return this.ui.winid } public get length(): number { return this.ui.length } public get defaultAction(): ListAction { let { defaultAction, actions, name } = this.list let config = workspace.getConfiguration(`list.source.${name}`) let action: ListAction if (config.defaultAction) action = actions.find(o => o.name == config.defaultAction) if (!action) action = actions.find(o => o.name == defaultAction) if (!action) action = actions[0] if (!action) throw new Error(`default action "${defaultAction}" not found`) return action } public async hide(notify = false, isVim = workspace.isVim): Promise { if (this.hidden) return let { nvim, timer, targetWinid, context } = this let { winid } = this.ui if (timer) clearTimeout(timer) this.worker.stop() this.history.add() this.ui.reset() db.save() this.hidden = true nvim.pauseNotification() if (!isVim) nvim.call('coc#prompt#stop_prompt', ['list'], true) if (winid) nvim.call('coc#list#close', [winid, context.options.position, targetWinid, this.savedHeight], true) if (notify) return nvim.resumeNotification(true, true) await nvim.resumeNotification(false) if (isVim) { // required on vim await wait(10) nvim.call('coc#prompt#stop_prompt', ['list'], true) nvim.redrawVim() } } public toggleMode(): void { let mode: ListMode = this.prompt.mode == 'normal' ? 'insert' : 'normal' this.prompt.mode = mode this.listOptions.mode = mode this.updateStatus() } public stop(): void { this.worker.stop() } public async resolveItem(): Promise { let index = this.ui.index let item = this.ui.getItem(index) if (!item || item.resolved) return let { list } = this if (typeof list.resolveItem === 'function') { let label = item.label let resolved = await Promise.resolve(list.resolveItem(item)) if (resolved && index == this.ui.index) { Object.assign(item, resolved, { resolved: true }) if (label == resolved.label) return this.ui.updateItem(item, index) } } } public async showHelp(): Promise { await this.hide() let { list, nvim } = this nvim.pauseNotification() nvim.command(`tabe +setl\\ previewwindow [LIST HELP]`, true) nvim.command('setl nobuflisted noswapfile buftype=nofile bufhidden=wipe', true) await nvim.resumeNotification() let hasOptions = list.options && list.options.length let buf = await nvim.buffer let highlighter = new Highlighter() highlighter.addLine('NAME', 'Label') highlighter.addLine(` ${list.name} - ${list.description || ''}\n`) highlighter.addLine('SYNOPSIS', 'Label') highlighter.addLine(` :CocList [LIST OPTIONS] ${list.name}${hasOptions ? ' [ARGUMENTS]' : ''}\n`) if (list.detail) { highlighter.addLine('DESCRIPTION', 'Label') let lines = list.detail.split('\n').map(s => ' ' + s) highlighter.addLine(lines.join('\n') + '\n') } if (hasOptions) { highlighter.addLine('ARGUMENTS', 'Label') highlighter.addLine('') for (let opt of list.options) { highlighter.addLine(opt.name, 'Special') highlighter.addLine(` ${opt.description}`) highlighter.addLine('') } highlighter.addLine('') } let config = workspace.getConfiguration(`list.source.${list.name}`) if (Object.keys(config).length) { highlighter.addLine('CONFIGURATIONS', 'Label') highlighter.addLine('') for (let key of Object.keys(config)) { let val = config[key] let name = `list.source.${list.name}.${key}` let description = defaultValue(workspace.configurations.getDescription(name), key) highlighter.addLine(` "${name}"`, 'MoreMsg') highlighter.addText(` - ${description} current value: `) highlighter.addText(JSON.stringify(val), 'Special') } highlighter.addLine('') } highlighter.addLine('ACTIONS', 'Label') highlighter.addLine(` ${list.actions.map(o => o.name).join(', ')}`) highlighter.addLine('') highlighter.addLine(`see ':h coc-list-options' for available list options.`, 'Comment') nvim.pauseNotification() highlighter.render(buf, 0, -1) nvim.command('setl nomod', true) nvim.command('setl nomodifiable', true) nvim.command('normal! gg', true) nvim.command('nnoremap q :bd!', true) await nvim.resumeNotification() } public async switchMatcher(): Promise { let { matcher, interactive } = this.listOptions if (interactive) return const list: Matcher[] = ['fuzzy', 'strict', 'regex'] let idx = list.indexOf(matcher) + 1 if (idx >= list.length) idx = 0 this.listOptions.matcher = list[idx] this.prompt.matcher = list[idx] await this.worker.drawItems() } private updateStatus(): void { let { ui, list, nvim } = this if (!ui.bufnr) return let buf = nvim.createBuffer(ui.bufnr) let status = { mode: this.prompt.mode.toUpperCase(), args: this.args.join(' '), name: list.name, cwd: this.cwd, loading: this.loadingFrame, total: this.worker.length } buf.setVar('list_status', status, true) nvim.command('redraws', true) } public get context(): ListContext { let { winid } = this.ui return { options: this.listOptions, args: this.listArgs, input: this.prompt.input, cwd: workspace.cwd, window: this.window, buffer: this.buffer, listWindow: winid ? this.nvim.createWindow(winid) : undefined } } private get window(): Window | undefined { return this.targetWinid ? this.nvim.createWindow(this.targetWinid) : undefined } private get buffer(): Buffer | undefined { return this.targetBufnr ? this.nvim.createBuffer(this.targetBufnr) : undefined } public onMouseEvent(key): Promise { switch (key) { case '': return this.ui.onMouse('mouseDown') case '': return this.ui.onMouse('mouseDrag') case '': return this.ui.onMouse('mouseUp') case '<2-LeftMouse>': return this.ui.onMouse('doubleClick') } } public async doNumberSelect(ch: string): Promise { if (!this.listOptions.numberSelect) return false let code = ch.charCodeAt(0) if (code >= 48 && code <= 57) { let n = Number(ch) if (n == 0) n = 10 if (this.ui.length >= n) { this.nvim.pauseNotification() this.ui.setCursor(n) await this.nvim.resumeNotification() await this.doAction() return true } } return false } public jumpBack(): void { let { targetWinid, nvim } = this if (targetWinid) { nvim.pauseNotification() nvim.call('coc#prompt#stop_prompt', ['list'], true) this.nvim.call('win_gotoid', [targetWinid], true) nvim.resumeNotification(false, true) } } public async resume(): Promise { if (this.winid) await this.hide() let res = await this.nvim.eval(`[win_getid(),bufnr("%"),${workspace.isVim ? 'winheight("%")' : 'nvim_win_get_height(0)'}]`) this.hidden = false this.targetWinid = res[0] this.targetBufnr = res[1] this.savedHeight = res[2] this.prompt.start() await this.ui.resume() if (this.listOptions.autoPreview) { await this.doAction('preview') } } private async doItemAction(items: ListItem[], action: ListAction): Promise { let { noQuit, position } = this.listOptions let { nvim } = this let persistAction = action.persist === true || action.name == 'preview' if (position === 'tab' && action.tabPersist) persistAction = true let persist = this.winid && (persistAction || noQuit) if (persist) { if (!persistAction) { nvim.pauseNotification() nvim.call('coc#prompt#stop_prompt', ['list'], true) nvim.call('win_gotoid', [this.context.window.id], true) await nvim.resumeNotification() } } else { await this.hide() } if (action.multiple) { await Promise.resolve(action.execute(items, this.context)) } else if (action.parallel) { await Promise.all(items.map(item => Promise.resolve(action.execute(item, this.context)))) } else { for (let item of items) { await Promise.resolve(action.execute(item, this.context)) } } if (persist) this.ui.restoreWindow() if (action.reload && persist) { await this.reloadItems() } else if (persist) { this.nvim.command('redraw', true) } } public onInputChange(): void { if (this.timer) clearTimeout(this.timer) this.ui.cancel() this.history.filter() this.listOptions.input = this.prompt.input // reload or filter items if (this.listOptions.interactive) { this.worker.stop() this.timer = setTimeout(async () => { await this.worker.loadItems(this.context) }, listConfiguration.debounceTime) } else { void this.worker.drawItems() } } public dispose(): void { void this.hide(true) disposeAll(this.disposables) this.worker.dispose() this.ui.dispose() } } ================================================ FILE: src/list/source/commands.ts ================================================ 'use strict' import commandManager from '../../commands' import Mru from '../../model/mru' import { ListContext, ListItem } from '../types' import { Extensions as ExtensionsInfo, IExtensionRegistry } from '../../util/extensionRegistry' import { Registry } from '../../util/registry' import workspace from '../../workspace' import BasicList from '../basic' import { formatListItems, UnformattedListItem } from '../formatting' import { toText } from '../../util/string' const extensionRegistry = Registry.as(ExtensionsInfo.ExtensionContribution) export default class CommandsList extends BasicList { public defaultAction = 'run' public description = 'registered commands of coc.nvim' public readonly name = 'commands' private mru: Mru constructor() { super() this.mru = workspace.createMru('commands') this.addAction('run', async item => { await commandManager.fireCommand(item.data.cmd) }) this.addAction('append', async item => { let { cmd } = item.data await workspace.nvim.feedKeys(`:CocCommand ${cmd} `, 'n', false) }) } public async loadItems(_context: ListContext): Promise { let items: UnformattedListItem[] = [] let mruList = await this.mru.load() let ids: Set = new Set() for (const obj of extensionRegistry.onCommands.concat(commandManager.commandList)) { let { id, title } = obj if (ids.has(id)) continue ids.add(id) let desc = toText(title) items.push({ label: [id, desc], filterText: id + ' ' + desc, data: { cmd: id, score: score(mruList, id) } }) } items.sort((a, b) => b.data.score - a.data.score) return formatListItems(this.alignColumns, items) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocCommandsTitle /\\t.*$/ contained containedin=CocCommandsLine', true) nvim.command('highlight default link CocCommandsTitle Comment', true) nvim.resumeNotification(false, true) } } function score(list: string[], key: string): number { let idx = list.indexOf(key) return idx == -1 ? -1 : list.length - idx } ================================================ FILE: src/list/source/diagnostics.ts ================================================ 'use strict' import { URI } from 'vscode-uri' import diagnosticManager, { DiagnosticItem } from '../../diagnostic/manager' import { severityLevel } from '../../diagnostic/util' import { defaultValue } from '../../util' import { isParentFolder } from '../../util/fs' import { path } from '../../util/node' import workspace from '../../workspace' import { formatListItems, formatPath, PathFormatting, UnformattedListItem } from '../formatting' import { ListManager } from '../manager' import { ListArgument, ListContext, ListItem } from '../types' import LocationList from './location' export function convertToLabel(item: DiagnosticItem, cwd: string, includeCode: boolean, pathFormat: PathFormatting = 'full'): string[] { const file = isParentFolder(cwd, item.file) ? path.relative(cwd, item.file) : item.file const formattedPath = formatPath(pathFormat, file) const formattedPosition = pathFormat !== "hidden" ? [`${formattedPath}:${item.lnum}`] : [] const source = includeCode ? `[${item.source} ${defaultValue(item.code, '')}]` : item.source return [...formattedPosition, source, item.severity, item.message] } export default class DiagnosticsList extends LocationList { public readonly defaultAction = 'open' public readonly description = 'diagnostics of current workspace' public name = 'diagnostics' public options: ListArgument[] = [{ name: '--buffer', hasValue: false, description: 'list diagnostics of current buffer only', }, { name: '--workspace-folder', hasValue: false, description: 'list diagnostics of current workspace folder only', }, { name: '-l, -level LEVEL', hasValue: true, description: 'filter diagnostics by diagnostic level, could be "error", "warning" and "information"' }] public constructor(manager: ListManager, event = true) { super() if (event) { diagnosticManager.onDidRefresh(async () => { let session = manager.getSession('diagnostics') if (session) await session.reloadItems() }, null, this.disposables) } } public async filterDiagnostics(parsedArgs: { [key: string]: string | boolean }): Promise { let list = await diagnosticManager.getDiagnosticList() if (parsedArgs['workspace-folder']) { const folder = workspace.getWorkspaceFolder(workspace.root) if (folder) { const normalized = URI.parse(folder.uri) list = list.filter(item => isParentFolder(normalized.fsPath, item.file)) } } else if (parsedArgs.buffer) { const doc = await workspace.document const normalized = URI.parse(doc.uri) list = list.filter(item => item.file === normalized.fsPath) } if (typeof parsedArgs.level === 'string') { let level = severityLevel(parsedArgs.level) list = list.filter(item => item.level <= level) } return list } public async loadItems(context: ListContext): Promise { let { cwd, args } = context const parsedArgs = this.parseArguments(args) let list = await this.filterDiagnostics(parsedArgs) const config = this.getConfig() const includeCode = config.get('includeCode', true) const pathFormat = config.get('pathFormat', "full") const unformatted: UnformattedListItem[] = list.map(item => { return { label: convertToLabel(item, cwd, includeCode, pathFormat), location: item.location, } }) return formatListItems(this.alignColumns, unformatted) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocDiagnosticsFile /\\v^\\s*\\S+/ contained containedin=CocDiagnosticsLine', true) nvim.command('syntax match CocDiagnosticsError /\\tError\\s*\\t/ contained containedin=CocDiagnosticsLine', true) nvim.command('syntax match CocDiagnosticsWarning /\\tWarning\\s*\\t/ contained containedin=CocDiagnosticsLine', true) nvim.command('syntax match CocDiagnosticsInfo /\\tInformation\\s*\\t/ contained containedin=CocDiagnosticsLine', true) nvim.command('syntax match CocDiagnosticsHint /\\tHint\\s*\\t/ contained containedin=CocDiagnosticsLine', true) nvim.command('highlight default link CocDiagnosticsFile Comment', true) nvim.command('highlight default link CocDiagnosticsError CocErrorSign', true) nvim.command('highlight default link CocDiagnosticsWarning CocWarningSign', true) nvim.command('highlight default link CocDiagnosticsInfo CocInfoSign', true) nvim.command('highlight default link CocDiagnosticsHint CocHintSign', true) nvim.resumeNotification(false, true) } } ================================================ FILE: src/list/source/extensions.ts ================================================ 'use strict' import { URI } from 'vscode-uri' import { ExtensionManager } from '../../extension/manager' import extensions from '../../extension' import { getConditionValue, wait } from '../../util' import { fs, os, path } from '../../util/node' import workspace from '../../workspace' import BasicList from '../basic' import { formatListItems, UnformattedListItem } from '../formatting' import { ListItem } from '../types' const delay = getConditionValue(50, 0) interface ItemToSort { data: { priority?: number, id?: string } } export default class ExtensionList extends BasicList { public defaultAction = 'toggle' public description = 'manage coc extensions' public name = 'extensions' constructor(private manager: ExtensionManager) { super() this.addAction('toggle', async item => { let { id, state } = item.data if (state == 'disabled') return if (state == 'activated') { await this.manager.deactivate(id) } else { await this.manager.activate(id) } await wait(delay) }, { persist: true, reload: true, parallel: true }) this.addAction('configuration', async item => { let { root } = item.data let jsonFile = path.join(root, 'package.json') if (fs.existsSync(jsonFile)) { let lines = fs.readFileSync(jsonFile, 'utf8').split(/\r?\n/) let idx = lines.findIndex(s => s.includes('"contributes"')) await workspace.jumpTo(URI.file(jsonFile), { line: idx == -1 ? 0 : idx, character: 0 }) } }) this.addAction('open', async item => { let { root } = item.data workspace.nvim.call('coc#ui#open_url', [root], true) }) this.addAction('disable', async item => { let { id, state } = item.data if (state !== 'disabled') await this.manager.toggleExtension(id) }, { persist: true, reload: true, parallel: true }) this.addAction('enable', async item => { let { id, state } = item.data if (state == 'disabled') await this.manager.toggleExtension(id) }, { persist: true, reload: true, parallel: true }) this.addAction('lock', async item => { let { id } = item.data this.manager.states.setLocked(id, true) }, { persist: true, reload: true }) this.addAction('help', async item => { let { root } = item.data let files = fs.readdirSync(root, { encoding: 'utf8' }) let file = files.find(f => /^readme/i.test(f)) if (file) await workspace.jumpTo(URI.file(path.join(root, file))) }) this.addAction('reload', async item => { let { id } = item.data await this.manager.reloadExtension(id) }, { persist: true, reload: true }) this.addMultipleAction('uninstall', async items => { let ids = [] for (let item of items) { if (item.data.isLocal) continue ids.push(item.data.id) } await this.manager.uninstallExtensions(ids) }) } public async loadItems(): Promise { let items: (UnformattedListItem & ItemToSort)[] = [] let list = await extensions.getExtensionStates() for (let stat of list) { let prefix = getExtensionPrefix(stat.state) let root = fs.realpathSync(stat.root) let locked = stat.isLocked items.push({ label: [`${prefix} ${stat.id}${locked ? ' ' : ''}`, ...(stat.isLocal ? ['[RTP]'] : []), stat.version, root.replace(os.homedir(), '~')], filterText: stat.id, data: { id: stat.id, root, state: stat.state, isLocal: stat.isLocal, priority: getExtensionPriority(stat.state) } }) } items.sort(sortExtensionItem) return formatListItems(this.alignColumns, items) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocExtensionsActivated /\\v^\\*/ contained containedin=CocExtensionsLine', true) nvim.command('syntax match CocExtensionsLoaded /\\v^\\+/ contained containedin=CocExtensionsLine', true) nvim.command('syntax match CocExtensionsDisabled /\\v^-/ contained containedin=CocExtensionsLine', true) nvim.command('syntax match CocExtensionsName /\\v%3c\\S+/ contained containedin=CocExtensionsLine', true) nvim.command('syntax match CocExtensionsRoot /\\v\\t[^\\t]*$/ contained containedin=CocExtensionsLine', true) nvim.command('syntax match CocExtensionsLocal /\\v\\[RTP\\]/ contained containedin=CocExtensionsLine', true) nvim.command('highlight default link CocExtensionsActivated Special', true) nvim.command('highlight default link CocExtensionsLoaded Normal', true) nvim.command('highlight default link CocExtensionsDisabled Comment', true) nvim.command('highlight default link CocExtensionsName String', true) nvim.command('highlight default link CocExtensionsLocal MoreMsg', true) nvim.command('highlight default link CocExtensionsRoot Comment', true) nvim.resumeNotification(false, true) } } export function sortExtensionItem(a: ItemToSort, b: ItemToSort): number { if (a.data.priority != b.data.priority) { return b.data.priority - a.data.priority } return b.data.id > a.data.id ? 1 : -1 } export function getExtensionPrefix(state: string): string { let prefix = '+' if (state == 'disabled') { prefix = '-' } else if (state == 'activated') { prefix = '*' } else if (state == 'unknown') { prefix = '?' } return prefix } export function getExtensionPriority(stat: string): number { switch (stat) { case 'unknown': return 2 case 'activated': return 1 case 'disabled': return -1 default: return 0 } } ================================================ FILE: src/list/source/folders.ts ================================================ 'use strict' import { URI } from 'vscode-uri' import { isDirectory } from '../../util/fs' import window from '../../window' import workspace from '../../workspace' import BasicList from '../basic' import { ListItem } from '../types' export default class FoldList extends BasicList { public defaultAction = 'edit' public description = 'list of current workspace folders' public name = 'folders' constructor() { super() this.addAction('edit', async item => { let newPath = await this.nvim.call('input', ['Folder: ', item.label, 'dir']) as string if (!isDirectory(newPath)) { void window.showWarningMessage(`invalid path: ${newPath}`) return } workspace.workspaceFolderControl.renameWorkspaceFolder(item.label, newPath) }) this.addAction('delete', async item => { workspace.workspaceFolderControl.removeWorkspaceFolder(item.label) }, { reload: true, persist: true }) this.addAction('newfile', async (item, context) => { let file = await window.requestInput('File name', item.label + '/') if (!file) return await workspace.createFile(file, { overwrite: false, ignoreIfExists: true }) await this.jumpTo(URI.file(file).toString(), null, context) }) } public async loadItems(): Promise { return workspace.folderPaths.map(p => ({ label: p })) } } ================================================ FILE: src/list/source/links.ts ================================================ 'use strict' import { Location } from 'vscode-languageserver-types' import languages from '../../languages' import type { CancellationToken } from '../../util/protocol' import workspace from '../../workspace' import BasicList from '../basic' import { formatUri } from '../formatting' import { ListContext, ListItem } from '../types' export default class LinksList extends BasicList { public defaultAction = 'open' public description = 'links of current buffer' public name = 'links' constructor() { super() this.addAction('open', async item => { let { target } = item.data await workspace.openResource(target) }) this.addAction('jump', async item => { let { location } = item.data await workspace.jumpTo(location.uri, location.range.start) }) } public async loadItems(context: ListContext, token: CancellationToken): Promise { let buf = await context.window.buffer let doc = workspace.getAttachedDocument(buf.id) let items: ListItem[] = [] let links = await languages.getDocumentLinks(doc.textDocument, token) if (links == null) throw new Error('Links provider not found.') for (let link of links) { link = link.target ? link : await languages.resolveDocumentLink(link, token) if (link.target) { items.push({ label: formatUri(link.target, workspace.cwd), data: { target: link.target, location: Location.create(doc.uri, link.range) } }) } } return items } } ================================================ FILE: src/list/source/lists.ts ================================================ 'use strict' import Mru from '../../model/mru' import { toText } from '../../util/string' import BasicList from '../basic' import { formatListItems, UnformattedListItem } from '../formatting' import { IList, ListContext, ListItem } from '../types' export default class ListsList extends BasicList { public readonly name = 'lists' public readonly defaultAction = 'open' public readonly description = 'registered lists of coc.nvim' private mru: Mru = new Mru('lists') constructor(private readonly listMap: Map) { super() this.addAction('open', async item => { let { name } = item.data await this.mru.add(name) setTimeout(() => { this.nvim.command(`CocList ${name}`, true) }, 50) }) } public async loadItems(_context: ListContext): Promise { let items: UnformattedListItem[] = [] let mruList = await this.mru.load() for (let list of this.listMap.values()) { if (list.name == 'lists') continue items.push({ label: [list.name, toText(list.description)], data: { name: list.name, interactive: list.interactive, score: mruScore(mruList, list.name) } }) } items.sort((a, b) => b.data.score - a.data.score) return formatListItems(this.alignColumns, items) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocListsDesc /\\t.*$/ contained containedin=CocListsLine', true) nvim.command('highlight default link CocListsDesc Comment', true) nvim.resumeNotification(false, true) } } export function mruScore(list: string[], key: string): number { let idx = list.indexOf(key) return idx == -1 ? -1 : list.length - idx } ================================================ FILE: src/list/source/location.ts ================================================ 'use strict' import { CancellationToken } from 'vscode-languageserver-protocol' import { Location, Position, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../../commands' import { AnsiHighlight, LocationWithTarget, QuickfixItem } from '../../types' import { toArray } from '../../util/array' import { isParentFolder } from '../../util/fs' import { path } from '../../util/node' import { byteLength } from '../../util/string' import BasicList from '../basic' import { ListContext, ListItem } from '../types' export default class LocationList extends BasicList { public defaultAction = 'open' public description = 'show locations saved by g:coc_jump_locations variable' public name = 'location' constructor() { super() this.createAction({ name: 'refactor', multiple: true, execute: async (items: ListItem[]) => { let locations = items.map(o => o.location) await commands.executeCommand('editor.action.showRefactor', locations) } }) this.addLocationActions() } public formatFilepath(file: string): string { if (typeof global.formatFilepath === 'function') { return global.formatFilepath(file) + '' } return file } public async loadItems(context: ListContext, _token: CancellationToken): Promise { // filename, lnum, col, text, type let locs = await this.nvim.getVar('coc_jump_locations') as QuickfixItem[] locs = toArray(locs) let bufnr = context.buffer.id let ignoreFilepath = locs.every(o => o.bufnr == bufnr) let items: ListItem[] = locs.map(loc => { let filename = ignoreFilepath ? '' : loc.filename if (filename.length > 0 && path.isAbsolute(filename)) { filename = isParentFolder(context.cwd, filename) ? path.relative(context.cwd, filename) : filename } return this.createItem(filename, loc) }) return items } private createItem(filename: string, loc: QuickfixItem): ListItem { let uri = loc.uri ?? URI.file(loc.filename).toString() let label = '' const ansiHighlights: AnsiHighlight[] = [] let start = 0 filename = this.formatFilepath(filename) if (filename.length > 0) { label = filename + ' ' ansiHighlights.push({ span: [start, start + byteLength(filename)], hlGroup: 'Directory' }) } start = byteLength(label) let lnum = loc.lnum ?? loc.range.start.line + 1 let col = loc.col ?? byteLength(loc.text.slice(0, loc.range.start.character)) + 1 let position = `|${loc.type ? loc.type + ' ' : ''}${lnum} Col ${col}|` label += position ansiHighlights.push({ span: [start, start + byteLength(position)], hlGroup: 'LineNr' }) if (loc.type) { let hl = loc.type.toLowerCase() === 'error' ? 'Error' : 'WarningMsg' ansiHighlights.push({ span: [start + 1, start + byteLength(loc.type)], hlGroup: hl }) } if (loc.range && loc.range.start.line == loc.range.end.line) { let len = byteLength(label) + 1 let start = len + byteLength(loc.text.slice(0, loc.range.start.character)) let end = len + byteLength(loc.text.slice(0, loc.range.end.character)) ansiHighlights.push({ span: [start, end], hlGroup: 'Search' }) } label += ' ' + loc.text let filterText = `${filename}${loc.text.trim()}` let location: LocationWithTarget if (loc.range) { location = Location.create(uri, loc.range) } else { let start = Position.create(loc.lnum - 1, loc.col - 1) let end = Position.create((loc.end_lnum ?? loc.lnum) - 1, (loc.end_col ?? loc.col) - 1) location = Location.create(uri, Range.create(start, end)) } location.targetRange = loc.targetRange ? loc.targetRange : Range.create(lnum - 1, 0, lnum - 1, 99) return { label, location, filterText, ansiHighlights, } } } ================================================ FILE: src/list/source/notifications.ts ================================================ import window from '../../window' import BasicList from '../basic' import { ListItem } from '../types' export default class NotificationsList extends BasicList { public readonly defaultAction = 'clear' public readonly description = 'notifications history' public readonly name = 'notifications' constructor() { super() this.addAction('clear', async () => { window.notifications.clearHistory() }) } public async loadItems(): Promise { return window.notifications.history.map(item => { return { label: `${item.time} ${item.kind.toUpperCase().padEnd(7)} ${item.message}`, filterText: item.message } }) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocNotificationTime /\\v^\\s*\\S+/ contained containedin=CocNotificationsLine', true) nvim.command('syntax match CocNotificationInfo /\\/ contained containedin=CocNotificationsLine', true) nvim.command('syntax match CocNotificationError /\\/ contained containedin=CocNotificationsLine', true) nvim.command('syntax match CocNotificationWarning /\\/ contained containedin=CocNotificationsLine', true) nvim.command('highlight default link CocNotificationTime Comment', true) nvim.command('highlight default link CocNotificationInfo CocInfoSign', true) nvim.command('highlight default link CocNotificationError CocErrorSign', true) nvim.command('highlight default link CocNotificationWarning CocWarningSign', true) nvim.resumeNotification(false, true) } } ================================================ FILE: src/list/source/outline.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { DocumentSymbol, Location, Range, SymbolInformation } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import languages from '../../languages' import Document from '../../model/document' import { isFalsyOrEmpty } from '../../util/array' import { getSymbolKind } from '../../util/convert' import { writeFile } from '../../util/fs' import { path, which } from '../../util/node' import { compareRangesUsingStarts } from '../../util/position' import { runCommand } from '../../util/processes' import type { CancellationToken } from '../../util/protocol' import workspace from '../../workspace' import { formatListItems, UnformattedListItem } from '../formatting' import { ListArgument, ListContext, ListItem } from '../types' import LocationList from './location' export default class Outline extends LocationList { public readonly description = 'symbols of current document' public name = 'outline' public options: ListArgument[] = [{ name: '-k, -kind KIND', hasValue: true, description: 'filter symbol by kind', }] public async loadItems(context: ListContext, token: CancellationToken): Promise { let document = workspace.getAttachedDocument(context.buffer.id) let config = this.getConfig() let ctagsFiletypes = config.get('ctagsFiletypes', []) let symbols: DocumentSymbol[] | null let args = this.parseArguments(context.args) let filterKind = args.kind ? args.kind.toString().toLowerCase() : null if (!ctagsFiletypes.includes(document.filetype)) { symbols = await languages.getDocumentSymbol(document.textDocument, token) } if (token.isCancellationRequested) return [] if (!symbols) return await loadCtagsSymbols(document, this.nvim, token) if (isFalsyOrEmpty(symbols)) return [] let items = symbolsToListItems(symbols, document.uri, filterKind) return formatListItems(this.alignColumns, items) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocOutlineName /\\v\\s?[^\\t]+\\s/ contained containedin=CocOutlineLine', true) nvim.command('syntax match CocOutlineIndentLine /\\v\\|/ contained containedin=CocOutlineLine,CocOutlineName', true) nvim.command('syntax match CocOutlineKind /\\[\\w\\+\\]/ contained containedin=CocOutlineLine', true) nvim.command('syntax match CocOutlineLine /\\d\\+$/ contained containedin=CocOutlineLine', true) nvim.command('highlight default link CocOutlineName Normal', true) nvim.command('highlight default link CocOutlineIndentLine Comment', true) nvim.command('highlight default link CocOutlineKind Typedef', true) nvim.command('highlight default link CocOutlineLine Comment', true) nvim.resumeNotification(false, true) } } export function symbolsToListItems(symbols: DocumentSymbol[], uri: string, filterKind: string | null): UnformattedListItem[] { let items: UnformattedListItem[] = [] const addSymbols = (symbols: DocumentSymbol[], level = 0) => { symbols.sort((a, b) => { return compareRangesUsingStarts(a.selectionRange, b.selectionRange) }) for (let s of symbols) { let kind = getSymbolKind(s.kind) let location = Location.create(uri, s.selectionRange) items.push({ label: [`${'| '.repeat(level)}${s.name}`, `[${kind}]`, `${s.range.start.line + 1}`], filterText: getFilterText(s, filterKind), location, data: { kind } }) if (!isFalsyOrEmpty(s.children)) { addSymbols(s.children, level + 1) } } } addSymbols(symbols as DocumentSymbol[]) if (filterKind) { items = items.filter(o => o.data.kind.toLowerCase().indexOf(filterKind) == 0) } return items } export function getFilterText(s: DocumentSymbol | SymbolInformation, kind: string | null): string { if (typeof kind === 'string' && kind.length > 0) return s.name return `${s.name}${getSymbolKind(s.kind)}` } export async function loadCtagsSymbols(document: Document, nvim: Neovim, token: CancellationToken): Promise { if (!which.sync('ctags', { nothrow: true })) { return [] } let uri = URI.parse(document.uri) let extname = path.extname(uri.fsPath) let content = '' let tempname = await nvim.call('tempname') as string let filepath = `${tempname}.${extname}` let cwd = path.dirname(tempname) let escaped = await nvim.call('fnameescape', filepath) as string await writeFile(escaped, document.getDocumentContent()) try { content = await runCommand(`ctags -f - --excmd=number --language-force=${document.filetype} ${escaped}`, { cwd }, token) } catch (e) { // noop } if (!content.trim().length) { content = await runCommand(`ctags -f - --excmd=number ${escaped}`, { cwd }, token) } content = content.trim() if (!content) return [] return contentToItems(content, document) } export function contentToItems(content: string, document: Document): ListItem[] { let lines = content.split(/\r?\n/) let items: ListItem[] = [] for (let line of lines) { let parts = line.split('\t') if (parts.length < 4) continue let lnum = Number(parts[2].replace(/;"$/, '')) let text = document.getline(lnum - 1) let idx = text.indexOf(parts[0]) let start = idx == -1 ? 0 : idx let range: Range = Range.create(lnum - 1, start, lnum - 1, start + parts[0].length) items.push({ label: `${parts[0]} [${parts[3]}] ${lnum}`, filterText: parts[0], location: Location.create(document.uri, range), data: { line: lnum } }) } items.sort((a, b) => a.data.line - b.data.line) return items } ================================================ FILE: src/list/source/services.ts ================================================ 'use strict' import services from '../../services' import { ListContext, ListItem } from '../types' import { wait } from '../../util' import BasicList from '../basic' import { formatListItems } from '../formatting' export default class ServicesList extends BasicList { public defaultAction = 'toggle' public description = 'registered services of coc.nvim' public name = 'services' constructor() { super() this.addAction('toggle', async item => { let { id } = item.data await services.toggle(id) await wait(50) }, { persist: true, reload: true }) } public async loadItems(_context: ListContext): Promise { let stats = services.getServiceStats() return formatListItems(this.alignColumns, stats.map(stat => { let prefix = stat.state == 'running' ? '*' : ' ' return { label: [prefix, stat.id, `[${stat.state}]`, stat.languageIds.join(', ')], data: { id: stat.id } } })) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocServicesPrefix /\\v^./ contained containedin=CocServicesLine', true) nvim.command('syntax match CocServicesName /\\v%3c\\S+/ contained containedin=CocServicesLine', true) nvim.command('syntax match CocServicesStat /\\v\\t\\[\\w+\\]/ contained containedin=CocServicesLine', true) nvim.command('syntax match CocServicesLanguages /\\v(\\])@<=.*$/ contained containedin=CocServicesLine', true) nvim.command('highlight default link CocServicesPrefix Special', true) nvim.command('highlight default link CocServicesName Type', true) nvim.command('highlight default link CocServicesStat Statement', true) nvim.command('highlight default link CocServicesLanguages Comment', true) nvim.resumeNotification(false, true) } } ================================================ FILE: src/list/source/sources.ts ================================================ 'use strict' import { Location, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import sources from '../../completion/sources' import { ListItem } from '../types' import BasicList from '../basic' import { fixWidth } from '../formatting' export default class SourcesList extends BasicList { public readonly defaultAction = 'toggle' public readonly description = 'registered completion sources' public readonly name = 'sources' constructor() { super() this.addAction('toggle', async item => { let { name } = item.data sources.toggleSource(name) }, { persist: true, reload: true }) this.addAction('refresh', async item => { let { name } = item.data await sources.refresh(name) }, { persist: true, reload: true }) this.addAction('open', async (item, context) => { let { location } = item if (location) await this.jumpTo(location, null, context) }) } public async loadItems(): Promise { let stats = sources.sourceStats() return stats.map(stat => { let prefix = stat.disabled ? ' ' : '*' let location: Location if (stat.filepath) { location = Location.create(URI.file(stat.filepath).toString(), Range.create(0, 0, 0, 0)) } return { label: `${prefix} ${fixWidth(stat.name, 22)} ${fixWidth('[' + stat.shortcut + ']', 10)} ${fixWidth(stat.triggerCharacters.join(''), 10)} ${fixWidth(stat.priority.toString(), 3)} ${stat.filetypes.join(',')}`, location, data: { name: stat.name } } }) } public doHighlight(): void { let { nvim } = this nvim.pauseNotification() nvim.command('syntax match CocSourcesPrefix /\\v^./ contained containedin=CocSourcesLine', true) nvim.command('syntax match CocSourcesName /\\v%3c\\S+/ contained containedin=CocSourcesLine', true) nvim.command('syntax match CocSourcesType /\\v%25v.*%36v/ contained containedin=CocSourcesLine', true) nvim.command('syntax match CocSourcesPriority /\\v%46v.*%52v/ contained containedin=CocSourcesLine', true) nvim.command('syntax match CocSourcesFileTypes /\\v\\S+$/ contained containedin=CocSourcesLine', true) nvim.command('highlight default link CocSourcesPrefix Special', true) nvim.command('highlight default link CocSourcesName Type', true) nvim.command('highlight default link CocSourcesPriority Number', true) nvim.command('highlight default link CocSourcesFileTypes Comment', true) nvim.command('highlight default link CocSourcesType Statement', true) nvim.resumeNotification(false, true) } } ================================================ FILE: src/list/source/symbols.ts ================================================ 'use strict' import { Location, Range, SymbolTag, WorkspaceSymbol } from 'vscode-languageserver-types' import languages, { ProviderName } from '../../languages' import { AnsiHighlight, LocationWithTarget } from '../../types' import { defaultValue } from '../../util' import { toArray } from '../../util/array' import { getSymbolKind } from '../../util/convert' import { minimatch } from '../../util/node' import { CancellationToken, CancellationTokenSource } from '../../util/protocol' import { byteLength } from '../../util/string' import workspace from '../../workspace' import { formatUri } from '../formatting' import { ListContext, ListItem } from '../types' import LocationList from './location' interface ItemToSort { data: { score?: number kind?: number file?: string } } export default class Symbols extends LocationList { public readonly interactive = true public readonly description = 'search workspace symbols' public readonly detail = 'Symbols list is provided by server, it works on interactive mode only.' public fuzzyMatch = workspace.createFuzzyMatch() public name = 'symbols' public options = [{ name: '-k, -kind KIND', description: 'Filter symbols by kind.', hasValue: true }] public async loadItems(context: ListContext, token: CancellationToken): Promise { let { input } = context let args = this.parseArguments(context.args) let filterKind = args.kind ? args.kind.toString().toLowerCase() : '' if (!languages.hasProvider(ProviderName.WorkspaceSymbols, { uri: 'file:///1', languageId: '' })) { throw new Error('No workspace symbols provider registered') } let symbols = await languages.getWorkspaceSymbols(input, token) if (token.isCancellationRequested) return [] let config = this.getConfig() let excludes = config.get('excludes', []) let items: (ListItem & ItemToSort)[] = [] this.fuzzyMatch.setPattern(input, true) for (let s of symbols) { let kind = getSymbolKind(s.kind) if (filterKind && kind.toLowerCase() != filterKind) { continue } let file = formatUri(s.location.uri, workspace.cwd) if (excludes.some(p => minimatch(file, p))) { continue } let item = this.createListItem(input, s, kind, file) items.push(item) } this.fuzzyMatch.free() items.sort(sortSymbolItems) return items } public async resolveItem(item: ListItem): Promise { let symbolItem = item.data.original as WorkspaceSymbol // no need to resolve if (!symbolItem || Location.is(symbolItem.location)) return null let tokenSource = new CancellationTokenSource() let resolved = await languages.resolveWorkspaceSymbol(symbolItem, tokenSource.token) resolved = defaultValue(resolved, symbolItem) if (Location.is(resolved.location)) { symbolItem.location = resolved.location item.location = toTargetLocation(resolved.location) } return item } public createListItem(input: string, item: WorkspaceSymbol, kind: string, file: string): ListItem & ItemToSort { let { name } = item let label = '' let ansiHighlights: AnsiHighlight[] = [] // Normal Typedef Comment let parts = [name, `[${kind}]`, this.formatFilepath(file)] let highlights = ['Normal', 'Typedef', 'Comment'] for (let index = 0; index < parts.length; index++) { const text = parts[index] let start = byteLength(label) label += text let end = byteLength(label) if (index != parts.length - 1) { label += ' ' } ansiHighlights.push({ span: [start, end], hlGroup: highlights[index] }) if (index === 0 && ((toArray(item.tags)).includes(SymbolTag.Deprecated)) || item['deprecated']) { ansiHighlights.push({ span: [start, end], hlGroup: 'CocDeprecatedHighlight' }) } } let score = 0 if (input.length > 0) { let result = this.fuzzyMatch.matchHighlights(name, 'CocListSearch') if (result) { score = result.score ansiHighlights.push(...result.highlights) } } return { label, filterText: '', ansiHighlights, location: toTargetLocation(item.location), data: { original: item, input, kind: item.kind, file, score, } } } } export function toTargetLocation(location: Location | { uri: string }): LocationWithTarget | Location { if (!Location.is(location)) { return Location.create(location.uri, Range.create(0, 0, 0, 0)) } let loc: LocationWithTarget = Location.create(location.uri, Range.create(location.range.start, location.range.start)) loc.targetRange = location.range return loc } export function sortSymbolItems(a: ItemToSort, b: ItemToSort): number { if (a.data.score != b.data.score) { return b.data.score - a.data.score } if (a.data.kind != b.data.kind) { return a.data.kind - b.data.kind } return a.data.file.length - b.data.file.length } ================================================ FILE: src/list/types.ts ================================================ import type { Buffer, Window } from '@chemzqm/neovim' import type { ProviderResult } from '../provider/index' import type { LocationWithTarget } from '../types' import type { CancellationToken } from '../util/protocol' export interface LocationWithLine { uri: string line: string text?: string } export interface ListItem { label: string filterText?: string preselect?: boolean location?: LocationWithTarget | LocationWithLine | string data?: any ansiHighlights?: AnsiHighlight[] resolved?: boolean /** * A string that should be used when comparing this item * with other items, only used for fuzzy filter. */ sortText?: string converted?: boolean } export interface ListItemWithScore extends ListItem { score?: number } export interface AnsiHighlight { span: [number, number] hlGroup: string } export interface ListItemsEvent { items: ListItemWithScore[] finished: boolean sorted: boolean append?: boolean reload?: boolean } export type ListMode = 'normal' | 'insert' export type Matcher = 'strict' | 'fuzzy' | 'regex' export interface ListOptions { position: string reverse: boolean input: string ignorecase: boolean interactive: boolean sort: boolean mode: ListMode matcher: Matcher autoPreview: boolean numberSelect: boolean noQuit: boolean first: boolean height?: number } export interface ListContext { args: string[] input: string cwd: string options: ListOptions window: Window buffer: Buffer listWindow: Window } export interface ListAction { name: string persist?: boolean reload?: boolean parallel?: boolean multiple?: boolean tabPersist?: boolean // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type execute: Function } export interface SingleListAction extends ListAction { multiple?: false execute: (item: ListItem, context: ListContext) => ProviderResult } export interface MultipleListAction extends ListAction { multiple: boolean execute: (item: ListItem[], context: ListContext) => ProviderResult } export interface ListTask { on(event: 'data', callback: (item: ListItem) => void): void on(event: 'end', callback: () => void): void on(event: 'error', callback: (msg: string | Error) => void): void dispose(): void } export interface ListArgument { key?: string hasValue?: boolean name: string description: string } export interface IList { /** * Unique name of list. */ name: string /** * Action list. */ actions: ListAction[] /** * Default action name. */ defaultAction: string /** * Load list items. */ loadItems(context: ListContext, token: CancellationToken): Promise /** * Resolve list item. */ resolveItem?(item: ListItem): Promise /** * Should be true when interactive is supported. */ interactive?: boolean /** * Description of list. */ description?: string /** * Detail description, shown in help. */ detail?: string /** * Options supported by list. */ options?: ListArgument[] /** * Highlight buffer by vim's syntax commands. */ doHighlight?(): void dispose?(): void } ================================================ FILE: src/list/ui.ts ================================================ 'use strict' import { Buffer, Neovim, Window } from '@chemzqm/neovim' import events from '../events' import { HighlightItem } from '../types' import { defaultValue, disposeAll, getConditionValue } from '../util' import { toArray } from '../util/array' import { debounce } from '../util/node' import { Disposable, Emitter, Event } from '../util/protocol' import { Sequence } from '../util/sequence' import { toText } from '../util/string' import workspace from '../workspace' import listConfiguration from './configuration' import { ListItem, ListItemsEvent, ListOptions } from './types' export type MouseEvent = 'mouseDown' | 'mouseDrag' | 'mouseUp' | 'doubleClick' export interface MousePosition { winid: number lnum: number col: number current: boolean } export interface HighlightGroup { hlGroup: string priority: number pos: [number, number, number] } const debounceTime = getConditionValue(100, 20) export default class ListUI { private window: Window private height: number public tabnr: number private newTab = false private reversed = false private buffer: Buffer private currIndex = 0 private items: ListItem[] = [] private disposables: Disposable[] = [] private selected: Set = new Set() private mouseDown: MousePosition private sequence = new Sequence() private _onDidChangeLine = new Emitter() private _onDidOpen = new Emitter() private _onDidClose = new Emitter() private _onDidLineChange = new Emitter() private _onDoubleClick = new Emitter() public readonly onDidChangeLine: Event = this._onDidChangeLine.event public readonly onDidLineChange: Event = this._onDidLineChange.event public readonly onDidOpen: Event = this._onDidOpen.event public readonly onDidClose: Event = this._onDidClose.event public readonly onDidDoubleClick: Event = this._onDoubleClick.event constructor( private nvim: Neovim, private name: string, private listOptions: ListOptions ) { this.newTab = listOptions.position == 'tab' this.reversed = listOptions.reverse === true events.on('BufWinLeave', async bufnr => { if (bufnr != this.bufnr || this.window == null) return this.window = null this._onDidClose.fire(bufnr) }, null, this.disposables) events.on('WinClosed', winid => { if (this.winid == winid) { let { bufnr } = this this.window = null this.buffer = null this._onDidClose.fire(bufnr) } }, null, this.disposables) events.on('CursorMoved', async (bufnr, cursor) => { if (bufnr != this.bufnr) return let idx = this.lnumToIndex(cursor[0]) this.onLineChange(idx) }, null, this.disposables) let debounced = debounce(async bufnr => { if (bufnr != this.bufnr) return let [winid, start, end] = await nvim.eval('[win_getid(),line("w0"),line("w$")]') as number[] if (end < 300 || winid != this.winid) return let h = end - start + 1 let s = this.lnumToIndex(start) let e = this.lnumToIndex(start + h * 2) nvim.pauseNotification() this.doHighlight(s, e) nvim.command('redraw', true) nvim.resumeNotification(false, true) }, debounceTime) this.disposables.push({ dispose: () => { debounced.clear() } }) events.on('CursorMoved', debounced, null, this.disposables) } public onDidChangeItems(ev: ListItemsEvent): void { if (!ev.append) this.clearSelection() this.sequence.run(async () => { let { items, reload, append, finished, sorted } = ev if (this.shouldSort && !sorted) { // do sort items = append ? this.items.concat(items) : items reload = append == true append = false items.sort((a, b) => { if (a.score != b.score) return b.score - a.score if (a.sortText > b.sortText) return 1 return -1 }) } if (append) { await this.appendItems(items) } else { await this.drawItems(items, finished, reload) } }) } public lnumToIndex(lnum: number): number { let { reversed, length } = this if (!reversed) return lnum - 1 return Math.max(0, length - lnum) } public indexToLnum(index: number): number { let { reversed, length } = this if (!reversed) return Math.min(index + 1, length) return Math.max(Math.min(length, length - index), 1) } public get bufnr(): number | undefined { return this.buffer?.id } public get winid(): number | undefined { return this.window?.id } private get limitLines(): number { return listConfiguration.get('limitLines', Infinity) } private onLineChange(index: number): void { if (this.currIndex == index) return this.currIndex = index this._onDidChangeLine.fire(index) } public get index(): number { return this.currIndex } public getItem(index: number): ListItem | undefined { return this.items[index] } public get item(): Promise { let { window } = this if (!window) return Promise.resolve(null) return window.cursor.then(cursor => { this.currIndex = this.lnumToIndex(cursor[0]) return this.items[this.currIndex] }) } public async echoMessage(item: ListItem): Promise { let { items } = this let idx = items.indexOf(item) let msg = `[${idx + 1}/${items.length}] ${toText(item.label)}` this.nvim.callTimer('coc#ui#echo_lines', [[msg]], true) } public updateItem(item: ListItem, index: number): void { if (!this.buffer || index >= this.length) return let { nvim } = this let lnum = this.indexToLnum(index) nvim.pauseNotification() this.buffer.setOption('modifiable', true, true) nvim.call('setbufline', [this.bufnr, lnum, item.label], true) this.doHighlight(index, index + 1) this.buffer.setOption('modifiable', false, true) nvim.resumeNotification(true, true) } public async getItems(): Promise { if (this.length == 0 || !this.window) return [] let mode = await this.nvim.call('mode') if (mode == 'v' || mode == 'V') { let [start, end] = await this.getSelectedRange() let res: ListItem[] = [] for (let i = start; i <= end; i++) { let idx = this.lnumToIndex(i) let item = this.items[idx] if (item) res.push(item) } return res } let { selectedItems } = this if (selectedItems.length) return selectedItems let item = await this.item return toArray(item) } public async onMouse(event: MouseEvent): Promise { let { nvim, window } = this if (!window) return let [winid, lnum, col] = await nvim.eval(`[v:mouse_winid,v:mouse_lnum,v:mouse_col]`) as [number, number, number] if (event == 'mouseDown') { this.mouseDown = { winid, lnum, col, current: winid == window.id } return } let current = winid == window.id if (current && event == 'doubleClick') { this.setCursor(lnum) this._onDoubleClick.fire() } if (current && event == 'mouseDrag') { if (!this.mouseDown) return await this.selectLines(this.mouseDown.lnum, lnum) } else if (current && event == 'mouseUp') { if (!this.mouseDown) return if (this.mouseDown.lnum == lnum) { this.setCursor(lnum) nvim.command('redraw', true) } else { await this.selectLines(this.mouseDown.lnum, lnum) } } else if (!current && event == 'mouseUp') { nvim.pauseNotification() nvim.call('win_gotoid', winid, true) nvim.call('cursor', [lnum, col], true) nvim.command('redraw', true) nvim.resumeNotification(false, true) } } public async resume(): Promise { let { items, selected, nvim } = this await this.drawItems(items, true, true) if (!selected.size || !this.buffer) return nvim.pauseNotification() for (let lnum of selected) { this.buffer.placeSign({ lnum, id: listConfiguration.signOffset + lnum, name: 'CocSelected', group: 'coc-list' }) } nvim.command('redraw', true) nvim.resumeNotification(false, true) } public async toggleSelection(): Promise { let { nvim, reversed } = this await nvim.call('win_gotoid', [this.winid]) let lnum = await nvim.call('line', '.') as number let mode = await nvim.call('mode') as string if (mode == 'v' || mode == 'V') { let [start, end] = await this.getSelectedRange() let reverse = start > end if (reverse) [start, end] = [end, start] for (let i = start; i <= end; i++) { this.toggleLine(i) } this.setCursor(end) nvim.command('redraw', true) await nvim.resumeNotification() return } nvim.pauseNotification() this.toggleLine(lnum) this.setCursor(reversed ? lnum - 1 : lnum + 1) nvim.command('redraw', true) await nvim.resumeNotification() } private toggleLine(lnum: number): void { let { selected, buffer } = this let exists = selected.has(lnum) const signOffset = listConfiguration.signOffset if (!exists) { selected.add(lnum) buffer.placeSign({ lnum, id: signOffset + lnum, name: 'CocSelected', group: 'coc-list' }) } else { selected.delete(lnum) buffer.unplaceSign({ id: signOffset + lnum, group: 'coc-list' }) } } public async selectLines(start: number, end: number): Promise { let { nvim, buffer, length } = this const signOffset = listConfiguration.signOffset this.clearSelection() let { selected } = this nvim.pauseNotification() let reverse = start > end if (reverse) [start, end] = [end, start] for (let i = start; i <= end; i++) { if (i > length) break selected.add(i) buffer.placeSign({ lnum: i, id: signOffset + i, name: 'CocSelected', group: 'coc-list' }) } this.setCursor(end) nvim.command('redraw', true) await nvim.resumeNotification() } public async selectAll(): Promise { let { length } = this if (length > 0) await this.selectLines(1, length) } public clearSelection(): void { let { selected, buffer } = this if (buffer && selected.size > 0) { buffer.unplaceSign({ group: 'coc-list' }) this.selected.clear() } } public get ready(): Promise { if (this.window) return Promise.resolve() return new Promise(resolve => { let disposable = this.onDidLineChange(() => { disposable.dispose() resolve() }) }) } public getHeight(len: number, finished: boolean): number { let { listOptions } = this if (typeof listOptions.height === 'number') return listOptions.height let height = listConfiguration.get('height', 10) if (finished && !listOptions.interactive && listOptions.input.length == 0) { height = Math.min(len, height) } return Math.max(1, height) } public async drawItems(items: ListItem[], finished: boolean, reload = false): Promise { const { nvim, name, listOptions } = this this.items = items.length > this.limitLines ? items.slice(0, this.limitLines) : items if (!this.window) { let height = this.getHeight(items.length, finished) let { position, numberSelect } = listOptions let [bufnr, winid, tabnr] = await nvim.call('coc#list#create', [position, height, name, numberSelect]) as [number, number, number] this.tabnr = tabnr this.height = height this.buffer = nvim.createBuffer(bufnr) let win = this.window = nvim.createWindow(winid) let statusSegments = listConfiguration.get('statusLineSegments') if (statusSegments) win.setOption('statusline', statusSegments.join(" "), true) this._onDidOpen.fire(this.bufnr) } const lines: string[] = [] let selectIndex = 0 this.items.forEach((item, idx) => { lines.push(item.label) if (!reload && selectIndex == 0 && item.preselect) selectIndex = idx }) let newIndex = reload ? this.currIndex : selectIndex this.setLines(lines, 0, newIndex) this._onDidLineChange.fire() } public async appendItems(items: ListItem[]): Promise { if (!this.window || items.length === 0) return let curr = this.items.length let remain = this.limitLines - curr if (remain > 0) { let append = remain < items.length ? items.slice(0, remain) : items this.items = this.items.concat(append) this.setLines(append.map(item => item.label), append.length, this.currIndex) } } public get shouldSort(): boolean { let { matcher, interactive } = this.listOptions if (interactive || matcher !== 'fuzzy') return false return true } public setLines(lines: string[], append: number, index: number): void { let { nvim, buffer, window, reversed, newTab } = this if (!buffer || !window) return nvim.pauseNotification() if (!append) { nvim.call('coc#compat#clear_matches', [window.id], true) if (!lines.length) { lines = ['No results, press ? on normal mode to get help.'] nvim.call('coc#compat#matchaddpos', ['Comment', [[1]], 99, window.id], true) } } buffer.setOption('modifiable', true, true) if (reversed) { let replacement = lines.reverse() if (append) { nvim.call('appendbufline', [buffer.id, 0, replacement], true) } else { buffer.setLines(replacement, { start: 0, end: -1, strictIndexing: false }, true) } } else { buffer.setLines(lines, { start: append ? -1 : 0, end: -1, strictIndexing: false }, true) } buffer.setOption('modifiable', false, true) if (reversed && !newTab) { let maxHeight = listConfiguration.get('height', 10) nvim.call('coc#window#set_height', [window.id, Math.max(Math.min(maxHeight, this.length), 1)], true) } if (index > this.items.length - 1) index = 0 if (index == 0) { if (append == 0) { this.doHighlight(0, 299) } else { let s = this.length - append - 1 if (s < 300) this.doHighlight(s, Math.min(299, this.length - 1)) } } else { let height = newTab ? workspace.env.lines : this.height this.doHighlight(Math.max(0, index - height), Math.min(index + height + 1, this.length - 1)) } if (!append) { this.currIndex = index let lnum = this.indexToLnum(index) window.setCursor([lnum, 0], true) nvim.call('coc#list#select', [buffer.id, lnum], true) } if (reversed) nvim.command('normal! zb', true) nvim.command('redraws', true) nvim.resumeNotification(true, true) } public restoreWindow(): void { if (this.newTab) return let { winid, height } = this if (winid && height) { this.nvim.call('coc#window#set_height', [winid, height], true) } } public get length(): number { return this.items.length } public get selectedItems(): ListItem[] { let { selected, items } = this let res: ListItem[] = [] for (let i of selected) { let idx = this.lnumToIndex(i) if (items[i - 1]) res.push(items[idx]) } return res } private doHighlight(start: number, end: number): void { let { items, reversed, length, buffer } = this const highlightItems: HighlightItem[] = [] const iterate = (i: number): void => { let lnum = this.indexToLnum(i) - 1 let { ansiHighlights } = items[i] if (ansiHighlights) { for (let hi of ansiHighlights) { let { span, hlGroup } = hi highlightItems.push({ hlGroup, lnum, colStart: span[0], colEnd: span[1] }) } } } if (reversed) { for (let i = Math.min(end, length - 1); i >= start; i--) { iterate(i) } } else { for (let i = start; i <= Math.min(end, length - 1); i++) { iterate(i) } } start = this.indexToLnum(start) - 1 end = this.indexToLnum(end) - 1 if (start > end) { [start, end] = [end, start] } if (!buffer || highlightItems.length == 0) return buffer.updateHighlights('list', highlightItems, { start, end: end + 1, priority: 99 }) } public setCursor(lnum: number, col = 0, index?: number): void { let { items } = this let max = items.length == 0 ? 1 : items.length if (lnum > max) return // change index since CursorMoved event not fired (seems bug of neovim)! index = index == null ? this.lnumToIndex(lnum) : index this.onLineChange(index) this.window?.setCursor([lnum, col], true) this.nvim.call('coc#list#select', [this.bufnr, lnum], true) } public async setIndex(index: number): Promise { if (index < 0 || index >= this.items.length) return let { nvim } = this let lnum = this.indexToLnum(index) nvim.pauseNotification() this.setCursor(lnum, 0, index) nvim.command('redraw', true) await nvim.resumeNotification(false) } public async moveCursor(delta: number): Promise { let { index, reversed } = this await this.setIndex(reversed ? index - delta : index + delta) } private async getSelectedRange(): Promise<[number, number]> { let { nvim } = this await nvim.call('coc#prompt#stop_prompt', ['list']) await nvim.eval('feedkeys("\\", "in")') let [, start] = await nvim.call('getpos', "'<") as [number, number] let [, end] = await nvim.call('getpos', "'>") as [number, number] this.nvim.call('coc#prompt#start_prompt', ['list'], true) return [start, end] } public cancel(): void { this.sequence.cancel() } public reset(): void { this.cancel() if (this.window) { this.window = null this.buffer = null this.tabnr = undefined } } public dispose(): void { disposeAll(this.disposables) this.nvim.call('coc#window#close', [defaultValue(this.winid, -1)], true) this.reset() this.items = [] this._onDidChangeLine.dispose() this._onDidOpen.dispose() this._onDidClose.dispose() this._onDidLineChange.dispose() this._onDoubleClick.dispose() } } ================================================ FILE: src/list/worker.ts ================================================ 'use strict' import { createLogger } from '../logger' import { FuzzyMatch } from '../model/fuzzyMatch' import { defaultValue } from '../util' import { parseAnsiHighlights } from '../util/ansiparse' import { toArray } from '../util/array' import { filter } from '../util/async' import { patchLine } from '../util/diff' import { fuzzyMatch, getCharCodes } from '../util/fuzzy' import { Mutex } from '../util/mutex' import { CancellationToken, CancellationTokenSource, Emitter, Event } from '../util/protocol' import { bytes, smartcaseIndex, toText } from '../util/string' import workspace from '../workspace' import listConfiguration from './configuration' import Prompt from './prompt' import { IList, ListContext, ListItem, ListItemsEvent, ListItemWithScore, ListOptions, ListTask } from './types' const logger = createLogger('list-worker') const controlCode = '\x1b' const WHITE_SPACE_CHARS = [32, 9] const SEARCH_HL_GROUP = 'CocListSearch' export interface FilterOption { append?: boolean reload?: boolean } export type OnFilter = (arr: ListItem[], finished: boolean, sort?: boolean) => void // perform loading task export default class Worker { private _loading = false private _finished = false private mutex: Mutex = new Mutex() private filteredCount: number private totalItems: ListItem[] = [] private tokenSource: CancellationTokenSource private filterTokenSource: CancellationTokenSource private _onDidChangeItems = new Emitter() private _onDidChangeLoading = new Emitter() private fuzzyMatch: FuzzyMatch public readonly onDidChangeItems: Event = this._onDidChangeItems.event public readonly onDidChangeLoading: Event = this._onDidChangeLoading.event constructor( private list: IList, private prompt: Prompt, private listOptions: ListOptions ) { this.fuzzyMatch = workspace.createFuzzyMatch() } private set loading(loading: boolean) { if (this._loading == loading) return this._loading = loading this._onDidChangeLoading.fire(loading) } public get isLoading(): boolean { return this._loading } public async loadItems(context: ListContext, reload = false): Promise { this.cancelFilter() this.filteredCount = 0 this._finished = false let { list, listOptions } = this this.loading = true let { interactive } = listOptions this.tokenSource = new CancellationTokenSource() let token = this.tokenSource.token let items = await list.loadItems(context, token) if (token.isCancellationRequested) return items = items ?? [] if (Array.isArray(items)) { this.tokenSource = null this.totalItems = items this.loading = false this._finished = true let filtered: ListItem[] if (!interactive) { this.filterTokenSource = new CancellationTokenSource() await this.mutex.use(async () => { await this.filterItems(items as ListItem[], { reload }, token) }) } else { filtered = this.convertToHighlightItems(items) this._onDidChangeItems.fire({ sorted: true, items: filtered, reload, finished: true }) } } else { let task = items as ListTask let totalItems = this.totalItems = [] let taken = 0 let currInput = context.input this.filterTokenSource = new CancellationTokenSource() let _onData = async (finished?: boolean) => { await this.mutex.use(async () => { let inputChanged = this.input != currInput if (inputChanged) { currInput = this.input taken = defaultValue(this.filteredCount, 0) } if (taken >= totalItems.length) return let append = taken > 0 let remain = totalItems.slice(taken) taken = totalItems.length if (!interactive) { let tokenSource = this.filterTokenSource await this.filterItems(remain, { append, reload }, tokenSource.token) } else { let items = this.convertToHighlightItems(remain) this._onDidChangeItems.fire({ items, append, reload, sorted: true, finished }) } }) } let interval = setInterval(async () => { await _onData() }, 50) task.on('data', item => { totalItems.push(item) }) let onEnd = async () => { if (task == null) return clearInterval(interval) this.tokenSource = null task = null this.loading = false this._finished = true disposable.dispose() if (token.isCancellationRequested) return if (totalItems.length == 0) { this._onDidChangeItems.fire({ items: [], append: false, sorted: true, reload, finished: true }) return } await _onData(true) } let disposable = token.onCancellationRequested(() => { this.mutex.reset() task?.dispose() void onEnd() }) let toDispose = task task.on('error', async (error: Error | string) => { if (task == null) return task = null toDispose.dispose() this.tokenSource = null this.loading = false disposable.dispose() clearInterval(interval) workspace.nvim.call('coc#prompt#stop_prompt', ['list'], true) workspace.nvim.echoError(`Task error: ${error.toString()}`) logger.error('List task error:', error) }) task.on('end', onEnd) } } /* * Draw all items with filter if necessary */ public async drawItems(): Promise { let { totalItems } = this if (totalItems.length === 0) return this.cancelFilter() let tokenSource = this.filterTokenSource = new CancellationTokenSource() let token = tokenSource.token await this.mutex.use(async () => { if (token.isCancellationRequested) return let { totalItems } = this this.filteredCount = totalItems.length await this.filterItems(totalItems, {}, tokenSource.token) }) } public cancelFilter(): void { if (this.filterTokenSource) { this.filterTokenSource.cancel() this.filterTokenSource = null } } public stop(): void { this.cancelFilter() if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = null } this.loading = false } public get length(): number { return this.totalItems.length } private get input(): string { return this.prompt.input } /** * Add highlights for interactive list */ private convertToHighlightItems(items: ListItem[]): ListItem[] { let input = toText(this.input) if (input.length > 0) this.fuzzyMatch.setPattern(input) let res = items.map(item => { convertItemLabel(item) let search = input.length > 0 && item.filterText !== '' if (search) { let filterLabel = getFilterLabel(item) let results = this.fuzzyMatch.matchHighlights(filterLabel, SEARCH_HL_GROUP) item.ansiHighlights = Array.isArray(item.ansiHighlights) ? item.ansiHighlights.filter(o => o.hlGroup !== SEARCH_HL_GROUP) : [] if (results) item.ansiHighlights.push(...results.highlights) } return item }) this.fuzzyMatch.free() return res } private async filterItemsByInclude(input: string, items: ListItem[], token: CancellationToken, onFilter: OnFilter): Promise { let { ignorecase } = this.listOptions const smartcase = listConfiguration.smartcase let inputs = toInputs(input, listConfiguration.extendedSearchMode) if (ignorecase) inputs = inputs.map(s => s.toLowerCase()) await filter(items, item => { convertItemLabel(item) let spans: [number, number][] = [] let filterLabel = getFilterLabel(item) let byteIndex = bytes(filterLabel) let curr = 0 item.ansiHighlights = toArray(item.ansiHighlights).filter(o => o.hlGroup !== SEARCH_HL_GROUP) for (let input of inputs) { let label = filterLabel.slice(curr) let idx = indexOf(label, input, smartcase, ignorecase) if (idx === -1) break let end = idx + curr + input.length spans.push([byteIndex(idx + curr), byteIndex(end)]) curr = end } if (spans.length !== inputs.length) return false item.ansiHighlights.push(...spans.map(s => { return { span: s, hlGroup: SEARCH_HL_GROUP } })) return true }, onFilter, token) } private async filterItemsByRegex(input: string, items: ListItem[], token: CancellationToken, onFilter: OnFilter): Promise { let { ignorecase } = this.listOptions let flags = ignorecase ? 'iu' : 'u' let inputs = toInputs(input, listConfiguration.extendedSearchMode) let regexes = inputs.reduce((p, c) => { try { p.push(new RegExp(c, flags)) } catch (e) {} return p }, []) await filter(items, item => { convertItemLabel(item) item.ansiHighlights = toArray(item.ansiHighlights).filter(o => o.hlGroup !== SEARCH_HL_GROUP) let spans: [number, number][] = [] let filterLabel = getFilterLabel(item) let byteIndex = bytes(filterLabel) let curr = 0 for (let regex of regexes) { let ms = filterLabel.slice(curr).match(regex) if (ms == null) break let end = ms.index + curr + ms[0].length spans.push([byteIndex(ms.index + curr), byteIndex(end)]) curr = end } if (spans.length !== inputs.length) return false item.ansiHighlights.push(...spans.map(s => { return { span: s, hlGroup: SEARCH_HL_GROUP } })) return true }, onFilter, token) } private async filterItemsByFuzzyMatch(input: string, items: ListItem[], token: CancellationToken, onFilter: OnFilter): Promise { let { extendedSearchMode, smartcase } = listConfiguration let { sort } = this.listOptions let idx = 0 this.fuzzyMatch.setPattern(input, !extendedSearchMode) let codes = getCharCodes(input) if (extendedSearchMode) codes = codes.filter(c => !WHITE_SPACE_CHARS.includes(c)) await filter(items, item => { convertItemLabel(item) let filterLabel = getFilterLabel(item) let match = this.fuzzyMatch.matchHighlights(filterLabel, SEARCH_HL_GROUP) if (!match || (smartcase && !fuzzyMatch(codes, filterLabel))) return false let ansiHighlights = Array.isArray(item.ansiHighlights) ? item.ansiHighlights.filter(o => o.hlGroup != SEARCH_HL_GROUP) : [] ansiHighlights.push(...match.highlights) return { sortText: typeof item.sortText === 'string' ? item.sortText : String.fromCharCode(idx), score: match.score, ansiHighlights } }, (items, done) => { onFilter(items, done, sort) }, token) } private async filterItems(arr: ListItem[], opts: FilterOption, token: CancellationToken): Promise { let { input } = this if (input.length === 0) { let items = arr.map(item => { return convertItemLabel(item) }) this._onDidChangeItems.fire({ items, sorted: true, finished: this._finished, ...opts }) return } let called = false let itemsToSort: ListItemWithScore[] = [] const onFilter = (items: ListItemWithScore[], done: boolean, sort?: boolean) => { let finished = done && this._finished if (token.isCancellationRequested || (!finished && items.length == 0)) return if (sort) { itemsToSort.push(...items) if (done) this._onDidChangeItems.fire({ items: itemsToSort, append: false, sorted: false, reload: opts.reload, finished }) } else { let append = opts.append === true || called called = true this._onDidChangeItems.fire({ items, append, sorted: true, reload: opts.reload, finished }) } } switch (this.listOptions.matcher) { case 'strict': await this.filterItemsByInclude(input, arr, token, onFilter) break case 'regex': await this.filterItemsByRegex(input, arr, token, onFilter) break default: await this.filterItemsByFuzzyMatch(input, arr, token, onFilter) } } public dispose(): void { this.stop() } } function getFilterLabel(item: ListItem): string { return item.filterText != null ? patchLine(item.filterText, item.label) : item.label } export function toInputs(input: string, extendedSearchMode: boolean): string[] { return extendedSearchMode ? parseInput(input) : [input] } export function convertItemLabel(item: ListItem): ListItem { let { label, converted } = item if (converted) return item if (label.includes('\n')) { label = item.label = label.replace(/\r?\n.*/gm, '') } if (label.includes(controlCode)) { let { line, highlights } = parseAnsiHighlights(label) item.label = line if (!Array.isArray(item.ansiHighlights)) item.ansiHighlights = highlights } item.converted = true return item } export function indexOf(label: string, input: string, smartcase: boolean, ignorecase: boolean): number { if (smartcase) return smartcaseIndex(input, label) return ignorecase ? label.toLowerCase().indexOf(input.toLowerCase()) : label.indexOf(input) } /** * `a\ b` => [`a b`] * `a b` => ['a', 'b'] */ export function parseInput(input: string): string[] { let res: string[] = [] let startIdx = 0 let currIdx = 0 let prev = '' for (; currIdx < input.length; currIdx++) { let ch = input[currIdx] if (WHITE_SPACE_CHARS.includes(ch.charCodeAt(0))) { // find space if (prev && prev != '\\' && startIdx != currIdx) { res.push(input.slice(startIdx, currIdx)) startIdx = currIdx + 1 } } else { } prev = ch } if (startIdx != input.length) { res.push(input.slice(startIdx, input.length)) } return res.map(s => s.replace(/\\\s/g, ' ').trim()).filter(s => s.length > 0) } ================================================ FILE: src/logger/index.ts ================================================ 'use strict' import { FileLogger, textToLogLevel, ILogger } from './log' import { fs, path, os } from '../util/node' import { getConditionValue } from '../util' export { getTimestamp } from './log' export function resolveLogFilepath(): string { let file = process.env.NVIM_COC_LOG_FILE if (file) return file let dir = process.env.XDG_RUNTIME_DIR if (dir) { try { fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK) return path.join(dir, `coc-nvim-${process.pid}.log`) } catch (err) { // noop } } let tmpdir = os.tmpdir() dir = path.join(tmpdir, `coc.nvim-${process.pid}`) fs.mkdirSync(dir, { recursive: true }) return path.join(dir, `coc-nvim.log`) } export function emptyFile(filepath: string): void { if (fs.existsSync(filepath)) { // cleanup if exists try { fs.writeFileSync(filepath, '', { encoding: 'utf8', mode: 0o666 }) } catch (e) { // noop } } } const logfile = resolveLogFilepath() emptyFile(logfile) const level = getConditionValue(process.env.NVIM_COC_LOG_LEVEL || 'info', 'off') export const logger = new FileLogger(logfile, textToLogLevel(level), { color: !global.REVISION && process.platform !== 'win32', userFormatters: true }) export function getLoggerFile(): string { return logfile } export function createLogger(category = 'coc.nvim'): ILogger { return logger.createLogger(category) } ================================================ FILE: src/logger/log.ts ================================================ 'use strict' import { fs, inspect, path, promisify } from '../util/node' const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB export enum LogLevel { Trace, Debug, Info, Warning, Error, Off } const yellowOpen = '\x1B[33m' const yellowClose = '\x1B[39m' export const DEFAULT_LOG_LEVEL: LogLevel = LogLevel.Info export const toTwoDigits = (v: number) => v < 10 ? `0${v}` : v.toString() export const toThreeDigits = (v: number) => v < 10 ? `00${v}` : v < 100 ? `0${v}` : v.toString() export interface ILogger { readonly category: string getLevel(): LogLevel log(...args: any[]): void trace(...args: any[]): void debug(...args: any[]): void info(...args: any[]): void warn(...args: any[]): void error(...args: any[]): void fatal(...args: any[]): void mark(...args: any[]): void /** * An operation to flush the contents. Can be synchronous. */ flush(): Promise } export function textToLogLevel(level: string): LogLevel { let str = level.toLowerCase() switch (str) { case 'trace': return LogLevel.Trace case 'debug': return LogLevel.Debug case 'info': return LogLevel.Info case 'error': return LogLevel.Error case 'warn': case 'warning': return LogLevel.Warning case 'off': return LogLevel.Off default: return LogLevel.Info } } export function format(args: any, depth = 2, color = false, hidden = false): string { let result = '' for (let i = 0; i < args.length; i++) { let a = args[i] if (typeof a === 'object') { try { a = inspect(a, hidden, depth, color) } catch (e) {} } if (color && (typeof a === 'boolean' || typeof a === 'number')) { a = `${yellowOpen}${a}${yellowClose}` } result += (i > 0 ? ' ' : '') + a } return result } abstract class AbstractLogger { protected level: LogLevel = DEFAULT_LOG_LEVEL public setLevel(level: LogLevel): void { if (this.level !== level) { this.level = level } } public getLevel(): LogLevel { return this.level } } export interface LoggerConfiguration { userFormatters: boolean color: boolean depth: number // 2 showHidden: boolean } export class FileLogger extends AbstractLogger { private promise: Promise private backupIndex = 1 private config: LoggerConfiguration private useConsole = false private loggers: Map = new Map() constructor( private readonly fsPath: string, level: LogLevel, config: Partial ) { super() this.config = Object.assign({ userFormatters: true, color: false, depth: 2, showHidden: false }, config) this.setLevel(level) this.promise = this.initialize() } public switchConsole(): void { this.useConsole = !this.useConsole } private format(args: any[]): string { let { color, showHidden, depth } = this.config return format(args, depth, color, showHidden) } public createLogger(scope: string): ILogger { let logger = this.loggers.has(scope) ? this.loggers.get(scope) : { category: scope, mark: () => { // not used }, getLevel: () => { return this.getLevel() }, trace: (...args: any[]) => { if (this.level <= LogLevel.Trace) { this._log(LogLevel.Trace, scope, args, this.getCurrentTimestamp()) } }, debug: (...args: any[]) => { if (this.level <= LogLevel.Debug) { this._log(LogLevel.Debug, scope, args, this.getCurrentTimestamp()) } }, log: (...args: any[]) => { if (this.level <= LogLevel.Info) { this._log(LogLevel.Info, scope, args, this.getCurrentTimestamp()) } }, info: (...args: any[]) => { if (this.level <= LogLevel.Info) { this._log(LogLevel.Info, scope, args, this.getCurrentTimestamp()) } }, warn: (...args: any[]) => { if (this.level <= LogLevel.Warning) { this._log(LogLevel.Warning, scope, args, this.getCurrentTimestamp()) } }, error: (...args: any[]) => { if (this.level <= LogLevel.Error) { this._log(LogLevel.Error, scope, args, this.getCurrentTimestamp()) } }, fatal: (...args: any[]) => { if (this.level <= LogLevel.Error) { this._log(LogLevel.Error, scope, args, this.getCurrentTimestamp()) } }, /** * An operation to flush the contents. Can be synchronous. */ flush: () => { return this.promise } } this.loggers.set(scope, logger) return logger } private async initialize(): Promise { return Promise.resolve() } public shouldBackup(size: number): boolean { return size > MAX_FILE_SIZE } private _log(level: LogLevel, scope: string, args: any[], time: string): void { if (this.useConsole) { let method = level === LogLevel.Error ? 'error' : 'log' console[method](`${stringifyLogLevel(level)} [${scope}]`, format(args, null, true)) } else { let message = this.format(args) this.promise = this.promise.then(() => { let fn = async () => { let text: string if (this.config.userFormatters !== false) { let parts = [time, stringifyLogLevel(level), `(pid:${process.pid})`, `[${scope}]`] text = `${parts.join(' ')} - ${message}\n` } else { text = message } await promisify(fs.appendFile)(this.fsPath, text, { encoding: 'utf8', flag: 'a+' }) let stat = await promisify(fs.stat)(this.fsPath) if (this.shouldBackup(stat.size)) { let newFile = this.getBackupResource() await promisify(fs.rename)(this.fsPath, newFile) } } return fn() }).catch(err => { if (!global.REVISION) { console.error(err) } }) } } private getCurrentTimestamp(): string { const currentTime = new Date() return `${currentTime.getFullYear()}-${toTwoDigits(currentTime.getMonth() + 1)}-${toTwoDigits(currentTime.getDate())}T${getTimestamp(currentTime)}` } private getBackupResource(): string { this.backupIndex = this.backupIndex > 5 ? 1 : this.backupIndex return path.join(path.dirname(this.fsPath), `${path.basename(this.fsPath)}_${this.backupIndex++}`) } } export function stringifyLogLevel(level: LogLevel): string { switch (level) { case LogLevel.Debug: return 'DEBUG' case LogLevel.Error: return 'ERROR' case LogLevel.Info: return 'INFO' case LogLevel.Trace: return 'TRACE' case LogLevel.Warning: return 'WARN' } return '' } export function getTimestamp(date: Date): string { return `${toTwoDigits(date.getHours())}:${toTwoDigits(date.getMinutes())}:${toTwoDigits(date.getSeconds())}.${toThreeDigits(date.getMilliseconds())}` } ================================================ FILE: src/markdown/index.ts ================================================ 'use strict' import { marked } from 'marked' import { Documentation, HighlightItem } from '../types' import { parseAnsiHighlights } from '../util/ansiparse' import * as Is from '../util/is' import { stripAnsi } from '../util/node' import { byteIndex, byteLength } from '../util/string' import Renderer from './renderer' export interface MarkdownParseOptions { breaks?: boolean excludeImages?: boolean } export interface CodeBlock { /** * Must have filetype or hlgroup */ filetype?: string hlGroup?: string startLine: number // 0 based endLine: number } export interface DocumentInfo { lines: string[] highlights: HighlightItem[] codes: CodeBlock[] } enum FiletypeHighlights { Error = 'CocErrorFloat', Warning = 'CocWarningFloat', Info = 'CocInfoFloat', Hint = 'CocHintFloat', } const filetyepsMap = { js: 'javascript', ts: 'typescript', bash: 'sh' } const ACTIVE_HL_GROUP = 'CocFloatActive' const HEADER_PREFIX = '\x1b[35m' const DIVIDING_LINE_HI_GROUP = 'CocFloatDividingLine' const MARKDOWN = 'markdown' const DOTS = '```' const TXT = 'txt' const DIVIDE_CHARACTER = '─' const DIVIDE_LINE = '───' export function toFiletype(match: null | undefined | string): string { if (!match) return TXT let mapped = filetyepsMap[match] return Is.string(mapped) ? mapped : match } export function parseDocuments(docs: Documentation[], opts: MarkdownParseOptions = {}): DocumentInfo { let lines: string[] = [] let highlights: HighlightItem[] = [] let codes: CodeBlock[] = [] let idx = 0 for (let doc of docs) { let currline = lines.length let { content, filetype } = doc let hls = doc.highlights if (filetype == MARKDOWN) { let info = parseMarkdown(content, opts) codes.push(...info.codes.map(o => { o.startLine = o.startLine + currline o.endLine = o.endLine + currline return o })) highlights.push(...info.highlights.map(o => { o.lnum = o.lnum + currline return o })) lines.push(...info.lines) } else { let parts = content.trim().split(/\r?\n/) let hlGroup = FiletypeHighlights[doc.filetype] if (Is.string(hlGroup)) { codes.push({ hlGroup, startLine: currline, endLine: currline + parts.length }) } else { codes.push({ filetype: doc.filetype, startLine: currline, endLine: currline + parts.length }) } lines.push(...parts) } if (Array.isArray(hls)) { highlights.push(...hls.map(o => { return Object.assign({}, o, { lnum: o.lnum + currline }) })) } if (Array.isArray(doc.active)) { let arr = getHighlightItems(content, currline, doc.active) if (arr.length) highlights.push(...arr) } if (idx != docs.length - 1) { highlights.push({ lnum: lines.length, hlGroup: DIVIDING_LINE_HI_GROUP, colStart: 0, colEnd: -1 }) lines.push(DIVIDE_CHARACTER) // dividing line } idx = idx + 1 } return { lines, highlights, codes } } /** * Get 'CocSearch' highlights from offset range */ export function getHighlightItems(content: string, currline: number, active: [number, number]): HighlightItem[] { let res: HighlightItem[] = [] let [start, end] = active let lines = content.split(/\r?\n/) let used = 0 let inRange = false for (let i = 0; i < lines.length; i++) { let line = lines[i] if (!inRange) { if (used + line.length > start) { inRange = true let colStart = byteIndex(line, start - used) if (used + line.length > end) { let colEnd = byteIndex(line, end - used) inRange = false res.push({ colStart, colEnd, lnum: i + currline, hlGroup: ACTIVE_HL_GROUP }) break } else { let colEnd = byteLength(line) res.push({ colStart, colEnd, lnum: i + currline, hlGroup: ACTIVE_HL_GROUP }) } } } else { if (used + line.length > end) { let colEnd = byteIndex(line, end - used) res.push({ colStart: 0, colEnd, lnum: i + currline, hlGroup: ACTIVE_HL_GROUP }) inRange = false break } else { let colEnd = byteLength(line) res.push({ colStart: 0, colEnd, lnum: i + currline, hlGroup: ACTIVE_HL_GROUP }) } } used = used + line.length + 1 } return res } /** * Parse markdown for lines, highlights & codes */ export function parseMarkdown(content: string, opts: MarkdownParseOptions): DocumentInfo { marked.setOptions({ renderer: new Renderer(), gfm: true, breaks: Is.boolean(opts.breaks) ? opts.breaks : true, hooks: Renderer.hooks, }) let lines: string[] = [] let highlights: HighlightItem[] = [] let codes: CodeBlock[] = [] let currline = 0 let inCodeBlock = false let filetype: string let startLnum = 0 let parsed = marked(content) let links = Renderer.getLinks() parsed = parsed.replace(/\s*$/, '') if (links.length) { parsed = parsed + '\n\n' + links.join('\n') } let parsedLines = parsed.split(/\n/) for (let i = 0; i < parsedLines.length; i++) { let line = parsedLines[i] if (!line.length) { let pre = lines[lines.length - 1] // Skip current line when previous line is empty if (!pre) continue let next = parsedLines[i + 1] // Skip empty line when next is code block or hr or header if (!next || next.startsWith(DOTS) || next.startsWith(DIVIDE_CHARACTER)) continue lines.push(line) currline++ continue } if (opts.excludeImages && line.indexOf('![') !== -1) { line = line.replace(/\s*!\[.*?\]\(.*?\)/g, '') if (!stripAnsi(line).trim().length) continue } let ms = line.match(/^\s*```\s*(\S+)?/) if (ms) { if (!inCodeBlock) { let pre = parsedLines[i - 1] if (pre && /^\s*```\s*/.test(pre)) { lines.push('') currline++ } inCodeBlock = true filetype = toFiletype(ms[1]) startLnum = currline } else { inCodeBlock = false codes.push({ filetype, startLine: startLnum, endLine: currline }) } continue } if (inCodeBlock) { // no parse lines.push(line) currline++ continue } let res = parseAnsiHighlights(line, true) if (line === DIVIDE_LINE) { highlights.push({ hlGroup: DIVIDING_LINE_HI_GROUP, lnum: currline, colStart: 0, colEnd: -1 }) } else if (res.highlights) { for (let hi of res.highlights) { let { hlGroup, span } = hi highlights.push({ hlGroup, lnum: currline, colStart: span[0], colEnd: span[1] }) } } lines.push(res.line) currline++ } return { lines, highlights, codes } } ================================================ FILE: src/markdown/renderer.ts ================================================ 'use strict' import { toObject } from '../util/object' import { MarkedOptions } from 'marked' /** * Renderer for convert markdown to terminal string */ import * as styles from './styles' let TABLE_CELL_SPLIT = '^*||*^' let TABLE_ROW_WRAP = '*|*|*|*' let TABLE_ROW_WRAP_REGEXP = new RegExp(escapeRegExp(TABLE_ROW_WRAP), 'g') let COLON_REPLACER = '*#COLON|*' let COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER), 'g') // HARD_RETURN holds a character sequence used to indicate text has a // hard (no-reflowing) line break. Previously \r and \r\n were turned // into \n in marked's lexer- preprocessing step. So \r is safe to use // to indicate a hard (non-reflowed) return. let HARD_RETURN = /\r/g function identity(str: string): string { return str } function cleanUpHtml(input: string): string { return styles.gray(input.replace(/(<([^>]+)>)/ig, '')) } let defaultOptions = { code: identity, blockquote: identity, html: cleanUpHtml, heading: styles.magenta, firstHeading: styles.magenta, hr: identity, listitem: identity, list, table: identity, paragraph: identity, strong: styles.bold, em: styles.italic, codespan: styles.yellow, del: styles.strikethrough, link: styles.underline, href: styles.underline, text: identity, unescape: true, emoji: false, width: 80, showSectionPrefix: false, tab: 2, tableOptions: {} } export function fixHardReturn(text, reflow) { return reflow ? text.replace(HARD_RETURN, '\n') : text } function indentLines(indent, text) { return text.replace(/(^|\n)(.+)/g, '$1' + indent + '$2') } export function identify(indent, text) { if (!text) return text return indent + text.split('\n').join('\n' + indent) } let BULLET_POINT_REGEX = '\\*' let NUMBERED_POINT_REGEX = '\\d+\\.' let POINT_REGEX = '(?:' + [BULLET_POINT_REGEX, NUMBERED_POINT_REGEX].join('|') + ')' // Prevents nested lists from joining their parent list's last line function fixNestedLists(body, indent) { let regex = new RegExp( '' + '(\\S(?: | )?)' + // Last char of current point, plus one or two spaces // to allow trailing spaces '((?:' + indent + ')+)' + // Indentation of sub point '(' + POINT_REGEX + '(?:.*)+)$', 'gm' ) // Body of subpoint return body.replace(regex, '$1\n' + indent + '$2$3') } let isPointedLine = function(line, indent) { return line.match('^(?:' + indent + ')*' + POINT_REGEX) != null } export function toSpaces(str) { return ' '.repeat(str.length) } const SPECIAL_SPACE = '\0\0\0' const SPACE = ' ' export function toSpecialSpaces(str) { return SPECIAL_SPACE.repeat(str.length) } let BULLET_POINT = '* ' export function bulletPointLine(indent, line) { if (isPointedLine(line, indent)) { return line } if (!line.includes(SPECIAL_SPACE)) { return toSpecialSpaces(BULLET_POINT) + line } return line } function bulletPointLines(lines, indent) { let transform = bulletPointLine.bind(null, indent) return lines .split('\n') .filter(identity) .map(transform) .join('\n') } let numberedPoint = function(n) { return n + '. ' } export function numberedLine(indent, line, num) { return isPointedLine(line, indent) ? { num: num + 1, line: line.replace(BULLET_POINT, numberedPoint(num + 1)) } : { num, line: toSpaces(numberedPoint(num)) + line } } function numberedLines(lines, indent) { let transform = numberedLine.bind(null, indent) let num = 0 return lines .split('\n') .filter(identity) .map(line => { const numbered = transform(line, num) num = numbered.num return numbered.line }) .join('\n') } function list(body, ordered, indent) { body = body.trim() body = ordered ? numberedLines(body, indent) : bulletPointLines(body, indent) return body } function section(text) { return text + '\n\n' } function undoColon(str) { return str.replace(COLON_REPLACER_REGEXP, ':') } export function generateTableRow(text, escape = null) { if (!text) return [] escape = escape || identity let lines = escape(text).split('\n') let data = [] lines.forEach(function(line) { if (!line) return let parsed = line .replace(TABLE_ROW_WRAP_REGEXP, '') .split(TABLE_CELL_SPLIT) data.push(parsed.splice(0, parsed.length - 1)) }) return data } function escapeRegExp(str) { // eslint-disable-next-line no-useless-escape return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') } function unescapeEntities(html) { return html .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") } const links: Map = new Map() export interface RendererOptions { sanitize?: boolean } class Renderer { private o: any private tab: any private tableSettings: any // private emoji: any private unescape: any private transform: any constructor(public options: RendererOptions = {}, public highlightOptions: any = {}) { this.o = Object.assign({}, defaultOptions, options) this.tab = ' ' this.tableSettings = this.o.tableOptions // this.emoji = identity this.unescape = unescapeEntities this.highlightOptions = toObject(highlightOptions) this.transform = this.compose(undoColon, this.unescape) } public static hooks: MarkedOptions['hooks'] = { preprocess: str => str, postprocess: str => { return str.replace(new RegExp(SPECIAL_SPACE, 'g'), SPACE) } } public text(t: string): string { return this.o.text(t) } public code(code: string, lang: string, _escaped: boolean): string { return '``` ' + lang + '\n' + code + '\n```\n' } public blockquote(quote: string): string { return section(this.o.blockquote(identify(this.tab, quote.trim()))) } public html(html: string): string { return this.o.html(html) } public heading(text: string, level: number, _raw: any): string { text = this.transform(text) return section( level === 1 ? this.o.firstHeading(text) : this.o.heading(text) ) } public hr(): string { // NOTE: the '─' character is conveniently translated into a window-wide // horizontal rule by coc.nvim/autoload/coc/float.vim. Using this character // causes the horizontal rule to appear like a proper hr separator. In case // the user isn't benefiting from a floating window, we provide three // characters so that the hr doesn't deviate too significantly from // Markdown's normal '-'. return `───\n` } public list(body, ordered): string { body = this.o.list(body, ordered, this.tab) return section(fixNestedLists(indentLines(this.tab, body), this.tab)) } public listitem(text: string): string { let transform = this.compose(this.o.listitem, this.transform) let isNested = text.indexOf('\n') !== -1 if (isNested) text = text.trim() // Use BULLET_POINT as a marker for ordered or unordered list item return '\n' + BULLET_POINT + transform(text) } public checkbox(checked): string { return '[' + (checked ? 'X' : ' ') + '] ' } public paragraph(text: string): string { let transform = this.compose(this.o.paragraph, this.transform) text = transform(text) return section(text) } public table(header, body): string { const Table = require('cli-table') let table = new Table( Object.assign( {}, { head: generateTableRow(header)[0] }, this.tableSettings ) ) generateTableRow(body, this.transform).forEach(function(row) { table.push(row) }) return section(this.o.table(table.toString())) } public tablerow(content: string): string { return TABLE_ROW_WRAP + content + TABLE_ROW_WRAP + '\n' } public tablecell(content, _flags): string { return content + TABLE_CELL_SPLIT } public strong(text: string): string { return this.o.strong(text) } public em(text: string): string { text = fixHardReturn(text, this.o.reflowText) return this.o.em(text) } public codespan(text: string): string { text = fixHardReturn(text, this.o.reflowText) return this.o.codespan(text.replace(/:/g, COLON_REPLACER)) } public br(): string { return '\n' } public del(text: string): string { return this.o.del(text) } public link(href, title, text): string { let prot: string try { prot = decodeURIComponent(unescape(href)) .replace(/[^\w:]/g, '') .toLowerCase() } catch (e) { return '' } if (prot.startsWith('javascript:')) { return '' } if (text && href && text != href) { links.set(text, href) } if (text && text != href) return styles.blue(text) let out = this.o.href(href) return this.o.link(out) } public image(href, title, text): string { let out = '![' + text return out + '](' + href + ')' } // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type public compose(...funcs: Function[]): any { return (...args: any[]) => { for (let i = funcs.length; i-- > 0;) { args = [funcs[i].apply(this, args)] } return args[0] } } public static getLinks(): string[] { let res = [] for (let [text, href] of links.entries()) { res.push(`${styles.blue(text)}: ${href}`) } links.clear() return res } } export default Renderer ================================================ FILE: src/markdown/styles.ts ================================================ 'use strict' import { styles } from '../util/node' export function gray(str: string): string { return `${styles.gray.open}${str}${styles.gray.close}` } export function magenta(str: string): string { return `${styles.magenta.open}${str}${styles.magenta.close}` } export function bold(str: string): string { return `${styles.bold.open}${str}${styles.bold.close}` } export function underline(str: string): string { return `${styles.underline.open}${str}${styles.underline.close}` } export function strikethrough(str: string): string { return `${styles.strikethrough.open}${str}${styles.strikethrough.close}` } export function italic(str: string): string { return `${styles.italic.open}${str}${styles.italic.close}` } export function yellow(str: string): string { return `${styles.yellow.open}${str}${styles.yellow.close}` } export function green(str: string): string { return `${styles.green.open}${str}${styles.green.close}` } export function blue(str: string): string { return `${styles.blue.open}${str}${styles.blue.close}` } ================================================ FILE: src/model/bufferSync.ts ================================================ 'use strict' import type Documents from '../core/documents' import events, { VisibleEvent } from '../events' import { DidChangeTextDocumentParams } from '../types' import { disposeAll } from '../util' import * as Is from '../util/is' import { Disposable } from '../util/protocol' import type Document from './document' export interface SyncItem extends Disposable { onChange?(e: DidChangeTextDocumentParams): void onTextChange?(): void onVisible?(winid: number, region: Readonly<[number, number]>) } /** * Buffer sync support, document is always attached and not command line buffer. */ export default class BufferSync { private disposables: Disposable[] = [] private itemsMap: Map = new Map() constructor(private _create: (doc: Document) => T | undefined, documents: Documents) { let { disposables } = this for (let doc of documents.attached()) { this.create(doc) } documents.onDidOpenTextDocument(e => { this.create(documents.getDocument(e.bufnr)) }, null, disposables) documents.onDidChangeDocument(e => { this.onChange(e) }, null, disposables) documents.onDidCloseDocument(e => { this.delete(e.bufnr) }, null, disposables) events.on('LinesChanged', this.onTextChange, this, disposables) events.on('WindowVisible', this.onVisible, this, disposables) } private onTextChange(bufnr: number): void { let o = this.itemsMap.get(bufnr) if (o && Is.func(o.item.onTextChange)) { o.item.onTextChange() } } private onVisible(ev: VisibleEvent): void { let o = this.itemsMap.get(ev.bufnr) if (o && typeof o.item.onVisible === 'function') { o.item.onVisible(ev.winid, ev.region) } } public get items(): ReadonlyArray { return Array.from(this.itemsMap.values()).map(x => x.item) } public getItem(bufnr: number | string | undefined): T | undefined { if (bufnr == null) return undefined if (typeof bufnr === 'number') { return this.itemsMap.get(bufnr)?.item } let o = Array.from(this.itemsMap.values()).find(v => { return v.uri == bufnr }) return o ? o.item : undefined } public create(doc: Document): void { let o = this.itemsMap.get(doc.bufnr) if (o) o.item.dispose() let item = this._create(doc) if (item) this.itemsMap.set(doc.bufnr, { uri: doc.uri, item }) } private onChange(e: DidChangeTextDocumentParams): void { let o = this.itemsMap.get(e.bufnr) if (o && typeof o.item.onChange == 'function') { o.item.onChange(e) } } private delete(bufnr: number): void { let o = this.itemsMap.get(bufnr) if (o) { o.item.dispose() this.itemsMap.delete(bufnr) } } public reset(): void { for (let o of this.itemsMap.values()) { o.item.dispose() } this.itemsMap.clear() } public dispose(): void { disposeAll(this.disposables) for (let o of this.itemsMap.values()) { o.item.dispose() } this._create = undefined this.itemsMap.clear() } } ================================================ FILE: src/model/chars.ts ================================================ 'use strict' import { Range } from 'vscode-languageserver-types' import { waitImmediate } from '../util' import { intable } from '../util/array' import { hasOwnProperty } from '../util/object' import { CancellationToken } from '../util/protocol' import { isHighSurrogate } from '../util/string' // Word ranges from vim, tested by '\k' option when '@' in iskeyword option. const WORD_RANGES: [number, number][] = [[257, 893], [895, 902], [904, 1369], [1376, 1416], [1418, 1469], [1471, 1471], [1473, 1474], [1476, 1522], [1525, 1547], [1549, 1562], [1564, 1566], [1568, 1641], [1646, 1747], [1749, 1791], [1806, 2403], [2406, 2415], [2417, 3571], [3573, 3662], [3664, 3673], [3676, 3843], [3859, 3897], [3902, 3972], [3974, 4169], [4176, 4346], [4348, 4960], [4969, 5740], [5743, 5759], [5761, 5786], [5789, 5866], [5870, 5940], [5943, 6099], [6109, 6143], [6155, 8191], [10240, 10495], [10649, 10711], [10716, 10747], [10750, 11775], [11904, 12287], [12321, 12335], [12337, 12348], [12350, 64829], [64832, 65071], [65132, 65279], [65296, 65305], [65313, 65338], [65345, 65370], [65382, 65535]] const MAX_CODE_UNIT = 65535 const boundary = 19968 export function getCharCode(str: string): number | undefined { if (/^\d+$/.test(str)) return parseInt(str, 10) if (str.length > 0) return str.charCodeAt(0) return undefined } export function sameScope(a: number, b: number): boolean { if (a < boundary) return b < boundary return b >= boundary } export function detectLanguage(code: number): string { // 中文范围 if (code >= 0x4E00 && code <= 0x9FFF) return 'cn' // 日语平假名、片假名 if ((code >= 0x3040 && code <= 0x309F) || (code >= 0x30A0 && code <= 0x30FF)) return 'ja' // 韩语 if (code >= 0xAC00 && code <= 0xD7AF) return 'ko' return '' } export function* parseSegments(text: string, segmenterLocales: string): Iterable { if (Intl === undefined || typeof Intl['Segmenter'] !== 'function') { yield text return } let res: string[] = [] let items = new Intl['Segmenter'](segmenterLocales === '' ? undefined : segmenterLocales, { granularity: 'word' }).segment(text) for (let item of items) { if (item.isWordLike) { yield item.segment } } return res } export function splitKeywordOption(iskeyword: string): string[] { let res: string[] = [] let i = 0 let s = 0 let len = iskeyword.length for (; i < len; i++) { let c = iskeyword[i] if (i + 1 == len && s != len) { res.push(iskeyword.slice(s, len)) continue } if (c == ',') { let d = i - s if (d == 0) continue if (d == 1) { let p = iskeyword[i - 1] if (p == '^' || p == ',') { res.push(p == ',' ? ',' : '^,') s = i + 1 if (p == '^' && iskeyword[i + 1] == ',') { i++ s++ } continue } } res.push(iskeyword.slice(s, i)) s = i + 1 } } return res } export class IntegerRanges { /** * Sorted ranges without overlap */ constructor(private ranges: [number, number][] = [], public wordChars = false) { } public clone(): IntegerRanges { return new IntegerRanges(this.ranges.slice(), this.wordChars) } /** * Add new range */ public add(start: number, end?: number): void { // find newIndex, replace count, new start, new end let index = 0 let removeCount = 0 if (end != null && end < start) { let t = end end = start start = t } end = end == null ? start : end for (let r of this.ranges) { let [s, e] = r if (e < start) { index++ continue } if (s > end) break // overlap removeCount++ if (s < start) start = s if (e > end) { end = e break } } this.ranges.splice(index, removeCount, [start, end]) } public exclude(start: number, end?: number): void { if (end != null && end < start) { let t = end end = start start = t } end = end == null ? start : end let index = 0 let removeCount = 0 let created: [number, number][] = [] for (let r of this.ranges) { let [s, e] = r if (e < start) { index++ continue } if (s > end) break removeCount++ if (s < start) { created.push([s, start - 1]) } if (e > end) { created.push([end + 1, e]) break } } if (removeCount == 0 && created.length == 0) return this.ranges.splice(index, removeCount, ...created) } public flatten(): number[] { return this.ranges.reduce((p, c) => p.concat(c), []) } public includes(n: number): boolean { if (n > 256 && this.wordChars) return intable(n, WORD_RANGES) return intable(n, this.ranges) } public static fromKeywordOption(iskeyword: string): IntegerRanges { let range = new IntegerRanges() for (let part of splitKeywordOption(iskeyword)) { let exclude = part.length > 1 && part.startsWith('^') let method = exclude ? 'exclude' : 'add' if (exclude) part = part.slice(1) if (part === '@' && !exclude) { // all word class range.wordChars = true range[method](65, 90) range[method](97, 122) range[method](192, 255) } else if (part == '@-@') { range[method]('@'.charCodeAt(0)) } else if (part.length == 1 || /^\d+$/.test(part)) { range[method](getCharCode(part)) } else if (part.includes('-')) { let items = part.split('-', 2) let start = getCharCode(items[0]) let end = getCharCode(items[1]) if (start === undefined || end === undefined) continue range[method](start, end) } } return range } } export class Chars { public ranges: IntegerRanges constructor(keywordOption: string) { this.ranges = IntegerRanges.fromKeywordOption(keywordOption) } public addKeyword(ch: string): void { this.ranges.add(ch.codePointAt(0)) } public clone(): Chars { let chars = new Chars('') chars.ranges = this.ranges.clone() return chars } public isKeywordCode(code: number): boolean { if (code === 32 || code > MAX_CODE_UNIT) return false if (isHighSurrogate(code)) return false return this.ranges.includes(code) } public isKeywordChar(ch: string): boolean { let code = ch.charCodeAt(0) return this.isKeywordCode(code) } public isKeyword(word: string): boolean { for (let i = 0, l = word.length; i < l; i++) { if (!this.isKeywordChar(word[i])) return false } return true } public *iterateWords(text: string): Iterable<[number, number]> { let start = -1 let prevCode: number | undefined for (let i = 0, l = text.length; i < l; i++) { let code = text.charCodeAt(i) if (this.isKeywordCode(code)) { if (start == -1) { start = i } else if (prevCode !== undefined && !sameScope(prevCode, code)) { yield [start, i] start = i } } else { if (start != -1) { yield [start, i] start = -1 } } if (i === l - 1 && start != -1) { yield [start, i + 1] } prevCode = code } } public matchLine(line: string, segmenterLocales = undefined, min = 2, max = 1024): string[] { let res: Set = new Set() let l = line.length if (l > max) { line = line.slice(0, max) l = max } for (let [start, end] of this.iterateWords(line)) { if (end - start < min) continue let word = line.slice(start, end) let code = word.charCodeAt(0) if (segmenterLocales != null && code > 255) { if (segmenterLocales == '') { segmenterLocales = detectLanguage(code) } for (let text of parseSegments(word, segmenterLocales)) { res.add(text) } } else { res.add(word) } } return Array.from(res) } public async computeWordRanges(lines: ReadonlyArray, range: Range, token?: CancellationToken): Promise<{ [word: string]: Range[] }> { let s = range.start.line let e = range.end.line let res: { [word: string]: Range[] } = {} let ts = Date.now() for (let i = s; i <= e; i++) { let text = lines[i] if (text === undefined) break let sc = i === s ? range.start.character : 0 if (i === s) text = text.slice(sc) if (i === e) text = text.slice(0, range.end.character - sc) if (Date.now() - ts > 15) { if (token && token.isCancellationRequested) break await waitImmediate() ts = Date.now() } for (let [start, end] of this.iterateWords(text)) { let word = text.slice(start, end) let arr = hasOwnProperty(res, word) ? res[word] : [] arr.push(Range.create(i, start + sc, i, end + sc)) res[word] = arr } } return res } } ================================================ FILE: src/model/db.ts ================================================ 'use strict' import { fs, path } from '../util/node' import { toObject } from '../util/object' export default class DB { constructor(public readonly filepath: string) { } /** * Get data by key. * @param {string} key unique key allows dot notation. * @returns {any} */ public fetch(key: string | undefined): any { let obj = this.load() if (!key) return obj let parts = key.split('.') for (let part of parts) { if (typeof obj[part] === 'undefined') { return undefined } obj = obj[part] } return obj } /** * Check if key exists * @param {string} key unique key allows dot notation. */ public exists(key: string): boolean { let obj = this.load() let parts = key.split('.') for (let part of parts) { if (typeof obj[part] === 'undefined') { return false } obj = obj[part] } return true } /** * Delete data by key * @param {string} key unique key allows dot notation. */ public delete(key: string): void { let obj = this.load() let origin = obj let parts = key.split('.') let len = parts.length for (let i = 0; i < len; i++) { if (typeof obj[parts[i]] === 'undefined') { break } if (i == len - 1) { delete obj[parts[i]] fs.writeFileSync(this.filepath, JSON.stringify(origin, null, 2), 'utf8') break } obj = obj[parts[i]] } } /** * Save data with key * @param {string} key unique string that allows dot notation. * @param {number|null|boolean|string|{[index} data saved data. */ public push(key: string, data: number | null | boolean | string | { [index: string]: any }): void { let origin = toObject(this.load()) let obj = origin let parts = key.split('.') let len = parts.length for (let i = 0; i < len; i++) { let key = parts[i] if (i == len - 1) { obj[key] = data let dir = path.dirname(this.filepath) fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(this.filepath, JSON.stringify(origin, null, 2)) break } if (typeof obj[key] == 'undefined') { obj[key] = {} obj = obj[key] } else { obj = obj[key] } } } private load(): any { let dir = path.dirname(this.filepath) let exists = fs.existsSync(dir) if (!exists) { fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(this.filepath, '{}', 'utf8') return {} } try { let content = fs.readFileSync(this.filepath, 'utf8') return JSON.parse(content.trim()) } catch (e) { fs.writeFileSync(this.filepath, '{}', 'utf8') return {} } } /** * Empty db file. */ public clear(): void { let exists = fs.existsSync(this.filepath) if (!exists) return fs.writeFileSync(this.filepath, '{}', 'utf8') } /** * Remove db file. */ public destroy(): void { if (fs.existsSync(this.filepath)) { fs.unlinkSync(this.filepath) } } } ================================================ FILE: src/model/dialog.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Disposable, Emitter, Event } from '../util/protocol' import events from '../events' import { HighlightItem } from '../types' import { disposeAll } from '../util' import { toArray } from '../util/array' export interface DialogButton { /** * Used by callback, should >= 0 */ index: number text: string /** * Not shown when true */ disabled?: boolean } export interface DialogPreferences { rounded?: boolean maxWidth?: number maxHeight?: number floatHighlight?: string floatBorderHighlight?: string pickerButtons?: boolean pickerButtonShortcut?: boolean confirmKey?: string shortcutHighlight?: string } export interface DialogConfig { content: string /** * Optional title text. */ title?: string /** * show close button, default to true when not specified. */ close?: boolean /** * highlight group for dialog window, default to `"dialog.floatHighlight"` or 'CocFloating' */ highlight?: string /** * highlight items of content. */ highlights?: ReadonlyArray /** * highlight groups for border, default to `"dialog.borderhighlight"` or 'CocFloatBorder' */ borderhighlight?: string /** * Buttons as bottom of dialog. */ buttons?: DialogButton[] /** * index is -1 for window close without button click */ callback?: (index: number) => void } export class Dialog { private disposables: Disposable[] = [] private bufnr: number private readonly _onDidClose = new Emitter() public readonly onDidClose: Event = this._onDidClose.event constructor(private nvim: Neovim, private config: DialogConfig) { events.on('BufWinLeave', bufnr => { if (bufnr == this.bufnr) { this.dispose() if (config.callback) config.callback(-1) } }, null, this.disposables) let btns = toArray(config.buttons).filter(o => o.disabled != true) events.on('FloatBtnClick', (bufnr, idx) => { if (bufnr == this.bufnr) { this.dispose() if (config.callback) config.callback(btns[idx].index) } }, null, this.disposables) } private get lines(): string[] { return [...this.config.content.split(/\r?\n/)] } public async show(preferences: DialogPreferences): Promise { let { nvim } = this let { title, close, highlights, buttons } = this.config let borderhighlight = this.config.borderhighlight || preferences.floatBorderHighlight let highlight = this.config.highlight || preferences.floatHighlight let opts: any = { maxwidth: preferences.maxWidth || 80, } if (title) opts.title = title opts.close = +(close ?? 1) if (preferences.maxHeight) opts.maxHeight = preferences.maxHeight if (preferences.maxWidth) opts.maxWidth = preferences.maxWidth if (highlight) opts.highlight = highlight if (highlights) opts.highlights = highlights if (borderhighlight) opts.borderhighlight = [borderhighlight] if (buttons) opts.buttons = buttons.filter(o => !o.disabled).map(o => o.text) if (preferences.rounded) opts.rounded = 1 if (Array.isArray(opts.buttons)) opts.getchar = 1 let [_winid, bufnr] = await nvim.call('coc#dialog#create_dialog', [this.lines, opts]) as [number, number] this.bufnr = bufnr nvim.command('redraw', true) } public get winid(): Promise { if (!this.bufnr) return Promise.resolve(null) return this.nvim.call('bufwinid', [this.bufnr]) as Promise } public dispose(): void { this._onDidClose.fire() this.bufnr = undefined disposeAll(this.disposables) this.disposables = [] } } ================================================ FILE: src/model/document.ts ================================================ 'use strict' import { Buffer, Neovim, VimValue } from '@chemzqm/neovim' import { Buffer as NodeBuffer } from 'buffer' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import events from '../events' import { createLogger } from '../logger' import { BufferOption, DidChangeTextDocumentParams, HighlightItem, HighlightItemOption, TextDocumentContentChange } from '../types' import { toArray } from '../util/array' import { isVim } from '../util/constants' import { diffLines, getTextEdit } from '../util/diff' import { disposeAll, getConditionValue, sha256, wait, waitNextTick } from '../util/index' import { isUrl } from '../util/is' import { debounce, path } from '../util/node' import { equals, toObject } from '../util/object' import { emptyRange } from '../util/position' import { Disposable, Emitter, Event } from '../util/protocol' import { byteIndex, byteLength, byteSlice, characterIndex, toText } from '../util/string' import { applyEdits, filterSortEdits, getPositionFromEdits, getStartLine, mergeTextEdits, TextChangeItem, toTextChanges } from '../util/textedit' import { Chars } from './chars' import { LinesTextDocument } from './textdocument' const logger = createLogger('document') const MAX_EDITS = getConditionValue(200, 400) export type LastChangeType = 'insert' | 'change' | 'delete' export type VimBufferChange = [number, number, string[]] export interface Env { readonly filetypeMap: { [index: string]: string } readonly isCygwin: boolean } export interface ChangeInfo { lnum: number line: string changedtick: number } export interface CursorAndCol { cursor?: [number, number] col?: number } const debounceTime = getConditionValue(150, 15) // getText, positionAt, offsetAt export default class Document { public buftype: string public isIgnored = false public chars: Chars private eol = true private _disposed = false private _attached = false private _notAttachReason = '' private _previewwindow = false private _winid = -1 private _winids: number[] = [] private _filetype: string private _bufname: string private _commandLine = false private _applying = false private _uri: string private _changedtick: number private variables: { [key: string]: VimValue } private disposables: Disposable[] = [] private _textDocument: LinesTextDocument // real current lines private lines: ReadonlyArray = [] private _applyLines: ReadonlyArray public fireContentChanges: (() => void) & { clear(): void } & { flush(): void } public fetchContent: (() => void) & { clear(): void } & { flush(): void } private _onDocumentChange = new Emitter() public readonly onDocumentChange: Event = this._onDocumentChange.event constructor( public readonly buffer: Buffer, private nvim: Neovim, filetype: string, opts: BufferOption ) { this.fireContentChanges = debounce(() => { this._fireContentChanges() }, debounceTime) this.init(filetype, opts) } /** * Synchronize content */ public get content(): string { return this.syncLines.join('\n') + (this.eol ? '\n' : '') } public get attached(): boolean { return this._attached } /** * Synchronized textDocument. */ public get textDocument(): LinesTextDocument { return this._textDocument } private get syncLines(): ReadonlyArray { return this._textDocument.lines } public get version(): number { return this._textDocument.version } /** * Buffer number */ public get bufnr(): number { return this.buffer.id } public get bufname(): string { return this._bufname } public get filetype(): string { return this._filetype } public get uri(): string { return this._uri } public get isCommandLine(): boolean { return this._commandLine } /** * LanguageId of TextDocument, main filetype are used for combined filetypes * with '.' */ public get languageId(): string { let { _filetype } = this return _filetype.includes('.') ? _filetype.match(/(.*?)\./)[1] : _filetype } /** * Get current buffer changedtick. */ public get changedtick(): number { return this._changedtick } /** * Scheme of document. */ public get schema(): string { return URI.parse(this.uri).scheme } /** * Line count of current buffer. */ public get lineCount(): number { return this.lines.length } /** * Window ID when buffer create, could be -1 when no window associated. */ public get winid(): number { return this._winid } public get winids(): ReadonlyArray { return this._winids } /** * Returns if current document is opened with previewwindow * @deprecated */ public get previewwindow(): boolean { return this._previewwindow } /** * Initialize document model. */ private init(filetype: string, opts: BufferOption): void { let buftype = this.buftype = opts.buftype this._bufname = opts.bufname this._commandLine = opts.commandline === 1 this._previewwindow = !!opts.previewwindow this._winid = opts.winid this._winids = toArray(opts.winids) this.variables = toObject(opts.variables) this._changedtick = opts.changedtick this.eol = opts.eol == 1 this._uri = getUri(opts.fullpath, this.bufnr, buftype) if (Array.isArray(opts.lines)) { this.lines = opts.lines.map(line => toText(line)) this._attached = true this.attach() } else { this.lines = [] this._notAttachReason = getNotAttachReason(buftype, this.variables[`coc_enabled`] as number, opts.size) } this._filetype = filetype this.setIskeyword(opts.iskeyword, opts.lisp) this.createTextDocument(1, this.lines) } public get notAttachReason(): string { return this._notAttachReason } public attach(): void { let lines = this.lines const { bufnr } = this this.buffer.attach(true).then(res => { if (!res) fireDetach(this.bufnr) }, _e => { fireDetach(this.bufnr) }) const onLinesChange = (_buf: number | Buffer, tick: number | null, firstline: number, lastline: number, linedata: string[]) => { if (tick && tick > this._changedtick) { this._changedtick = tick lines = [...lines.slice(0, firstline), ...linedata, ...(lastline < 0 ? [] : lines.slice(lastline))] if (lines.length == 0) lines = [''] if (this._applying) { this._applyLines = lines return } this.lines = lines fireLinesChanged(bufnr) if (events.completing) return this.fireContentChanges() } } if (isVim) { this.buffer.listen('vim_lines', onLinesChange, this.disposables) } else { this.buffer.listen('lines', onLinesChange, this.disposables) this.buffer.listen('detach', () => { fireDetach(this.bufnr) }, this.disposables) } } /** * Check if document changed after last synchronize */ public get dirty(): boolean { // if (this.lines === this.syncLines) return false // return !equals(this.lines, this.syncLines) return this.lines !== this.syncLines } public get hasChanged(): boolean { if (!this.dirty) return false return !equals(this.lines, this.syncLines) } /** * Cursor position if document is current document */ public get cursor(): Position | undefined { let { cursor } = events if (cursor.bufnr !== this.bufnr) return undefined let content = toText(this.lines[cursor.lnum - 1]) return Position.create(cursor.lnum - 1, characterIndex(content, cursor.col - 1)) } private _fireContentChanges(edit?: TextEdit): void { if (this.lines === this.syncLines) return let textDocument = this._textDocument let changes: TextDocumentContentChange[] = [] if (!edit) edit = getTextEdit(textDocument.lines, this.lines, this.cursor, events.cursor.insert) let original: string if (edit) { original = textDocument.getText(edit.range) changes.push({ range: edit.range, text: edit.newText, rangeLength: original.length }) } else { original = '' } let created = this.createTextDocument(this.version + (edit ? 1 : 0), this.lines) this._onDocumentChange.fire(Object.freeze({ bufnr: this.bufnr, original, originalLines: textDocument.lines, textDocument: { version: created.version, uri: this.uri }, document: created, contentChanges: changes })) } public async applyEdits(edits: TextEdit[], joinUndo = false, move: boolean | Position = false): Promise { if (Array.isArray(arguments[1])) edits = arguments[1] if (!this._attached || edits.length === 0) return const { bufnr } = this this._forceSync() let textDocument = this.textDocument edits = filterSortEdits(textDocument, edits) // apply edits to current textDocument let newLines = applyEdits(textDocument, edits) if (!newLines) return let lines = textDocument.lines let changed = diffLines(lines, newLines, getStartLine(edits[0])) // append new lines let isAppend = changed.start === changed.end && changed.start === lines.length let original = lines.slice(changed.start, changed.end) let changes: TextChangeItem[] = [] // Avoid too many buf_set_text cause nvim slow. // Not used when insert or delete lines. if (edits.length <= MAX_EDITS && changed.start !== changed.end && changed.replacement.length > 0) { changes = toTextChanges(lines, edits) } const { cursor, col } = this.getCursorAndCol(move, edits, newLines) this.nvim.pauseNotification() if (joinUndo) this.nvim.command(`if bufnr('%') == ${bufnr} | undojoin | endif`, true) if (isAppend) { this.buffer.setLines(changed.replacement, { start: -1, end: -1 }, true) } else { this.nvim.call('coc#ui#set_lines', [ this.bufnr, this._changedtick, original, changed.replacement, changed.start, changed.end, changes, cursor, col, lines.length ], true) } this._applying = true void this.nvim.resumeNotification(true, true) this.lines = newLines await waitNextTick() fireLinesChanged(bufnr) let textEdit = edits.length == 1 ? edits[0] : mergeTextEdits(edits, lines, newLines) this.fireContentChanges.clear() this._fireContentChanges(textEdit) let range = Range.create(changed.start, 0, changed.start + changed.replacement.length, 0) return TextEdit.replace(range, original.join('\n') + (original.length > 0 ? '\n' : '')) } public onTextChange(): void { let { bufnr } = this if (this._applying) { this._applying = false if (this._applyLines != null && !equals(this._applyLines, this.textDocument.lines)) { this.lines = this._applyLines this._applyLines = undefined fireLinesChanged(bufnr) this.fireContentChanges() } } } private getCursorAndCol(move: boolean | Position, edits: TextEdit[], newLines: ReadonlyArray): CursorAndCol { if (!move) return {} let pos = Position.is(move) ? move : this.cursor if (pos) { let position = getPositionFromEdits(pos, edits) if (!equals(pos, position)) { let content = toText(newLines[position.line]) let column = byteIndex(content, position.character) + 1 return { cursor: [position.line + 1, column], col: byteIndex(this.lines[pos.line], pos.character) + 1 } } } return {} } public async changeLines(lines: [number, string][]): Promise { let filtered: [number, string][] = [] let newLines = this.lines.slice() for (let [lnum, text] of lines) { if (newLines[lnum] != text) { filtered.push([lnum, text]) newLines[lnum] = text } } if (!filtered.length) return this.nvim.call('coc#ui#change_lines', [this.bufnr, filtered], true) this.nvim.redrawVim() this.lines = newLines await waitNextTick() fireLinesChanged(this.bufnr) this._forceSync() } public _forceSync(): void { if (!this._attached) return this.fireContentChanges.clear() this._fireContentChanges() } public forceSync(): void { // may cause bugs, prevent extensions use it. if (global.__TEST__) { this._forceSync() } } /** * Get offset from lnum & col */ public getOffset(lnum: number, col: number): number { return this.textDocument.offsetAt({ line: lnum - 1, character: col }) } /** * Check string is word. */ public isWord(word: string): boolean { return this.chars.isKeyword(word) } public getStartWord(text: string): string { let i = 0 for (; i < text.length; i++) { if (!this.chars.isKeywordChar(text[i])) break } return text.slice(0, i) } /** * Current word for replacement */ public getWordRangeAtPosition(position: Position, extraChars?: string, current = true): Range | null { let chars = this.chars if (extraChars && extraChars.length) { chars = this.chars.clone() for (let ch of extraChars) { chars.addKeyword(ch) } } let line = this.getline(position.line, current) let ch = line[position.character] if (ch == null || !chars.isKeywordChar(ch)) return null let start = position.character let end = position.character + 1 while (start >= 0) { let ch = line[start - 1] if (!ch || !chars.isKeywordChar(ch)) break start = start - 1 } while (end <= line.length) { let ch = line[end] if (!ch || !chars.isKeywordChar(ch)) break end = end + 1 } return Range.create(position.line, start, position.line, end) } private createTextDocument(version: number, lines: ReadonlyArray): LinesTextDocument { let { uri, languageId, eol } = this let textDocument = this._textDocument = new LinesTextDocument(uri, languageId, version, lines, this.bufnr, eol) return textDocument } /** * Get ranges of word in textDocument. */ public getSymbolRanges(word: string): Range[] { let { version, languageId, uri } = this let textDocument = new LinesTextDocument(uri, languageId, version, this.lines, this.bufnr, this.eol) let res: Range[] = [] let content = textDocument.getText() let str = '' for (let i = 0, l = content.length; i < l; i++) { let ch = content[i] if ('-' == ch && str.length == 0) { continue } let isKeyword = this.chars.isKeywordChar(ch) if (isKeyword) { str = str + ch } if (str.length > 0 && !isKeyword && str == word) { res.push(Range.create(textDocument.positionAt(i - str.length), textDocument.positionAt(i))) } if (!isKeyword) { str = '' } } return res } /** * Adjust col with new valid character before position. */ public fixStartcol(position: Position, valids: string[]): number { let line = this.getline(position.line) if (!line) return 0 let { character } = position let start = line.slice(0, character) let col = byteLength(start) let { chars } = this for (let i = start.length - 1; i >= 0; i--) { let c = start[i] if (!chars.isKeywordChar(c) && !valids.includes(c)) { break } col = col - byteLength(c) } return col } /** * Add vim highlight items from highlight group and range. * Synchronized lines are used for calculate cols. */ public addHighlights(items: HighlightItem[], hlGroup: string, range: Range, opts: HighlightItemOption = {}): void { let { start, end } = range if (emptyRange(range)) return for (let line = start.line; line <= end.line; line++) { const text = this.getline(line, false) let colStart = line == start.line ? byteIndex(text, start.character) : 0 let colEnd = line == end.line ? byteIndex(text, end.character) : NodeBuffer.byteLength(text) if (colStart >= colEnd) continue items.push(Object.assign({ hlGroup, lnum: line, colStart, colEnd }, opts)) } } /** * Line content 0 based line */ public getline(line: number, current = true): string { if (current) return this.lines[line] || '' return this.syncLines[line] || '' } /** * Get lines, zero indexed, end exclude. */ public getLines(start?: number, end?: number): string[] { return this.lines.slice(start ?? 0, end ?? this.lines.length) } /** * Get current content text. */ public getDocumentContent(): string { let content = this.lines.join('\n') return this.eol ? content + '\n' : content } /** * Get variable value by key, defined by `b:coc_{key}` */ public getVar(key: string, defaultValue?: T): T { let val = this.variables[`coc_${key}`] as T return val === undefined ? defaultValue : val } /** * Get position from lnum & col */ public getPosition(lnum: number, col: number): Position { let line = this.getline(lnum - 1) if (!line || col == 0) return { line: lnum - 1, character: 0 } let pre = byteSlice(line, 0, col - 1) return { line: lnum - 1, character: pre.length } } /** * Recreate document with new filetype. */ public setFiletype(filetype: string): void { this._filetype = filetype let lines = this.lines this._textDocument = new LinesTextDocument(this.uri, this.languageId, 1, lines, this.bufnr, this.eol) } /** * Change iskeyword option of document */ public setIskeyword(iskeyword: string, lisp?: number): void { let chars = this.chars = new Chars(iskeyword) let additional = this.getVar('additional_keywords', []) if (lisp) chars.addKeyword('-') if (additional && Array.isArray(additional)) { for (let ch of additional) { chars.addKeyword(ch) } } } /** * Detach document. */ public detach(): void { disposeAll(this.disposables) if (this._disposed) return this._disposed = true this._attached = false this.lines = [] this.fireContentChanges.clear() this._onDocumentChange.dispose() } /** * Synchronize latest document content */ public async synchronize(): Promise { if (!this.attached) return let { changedtick } = this await this.patchChange() if (changedtick != this.changedtick) { await wait(30) } } /** * Synchronize buffer change */ public async patchChange(): Promise { if (!this._attached) return // changedtick from buffer events could be not latest. #3003 this._changedtick = await this.nvim.call('coc#util#get_changedtick', [this.bufnr]) as number this._forceSync() } public getSha256(): string { return sha256(this.lines.join('\n')) } public async fetchLines(): Promise { let lines = await this.nvim.call('getbufline', [this.bufnr, 1, '$']) as ReadonlyArray this.lines = lines fireLinesChanged(this.bufnr) this.fireContentChanges() logger.error(`Buffer ${this.bufnr} not synchronized on vim9, consider send bug report!`) } } function fireDetach(bufnr: number): void { void events.fire('BufDetach', [bufnr]) } function fireLinesChanged(bufnr: number): void { void events.fire('LinesChanged', [bufnr]) } export function getUri(fullpath: string, id: number, buftype: string): string { if (!fullpath) return `untitled:${id}` if (path.isAbsolute(fullpath)) return URI.file(path.normalize(fullpath)).toString() if (isUrl(fullpath)) return URI.parse(fullpath).toString() if (buftype != '') return `${buftype}:${id}` return `unknown:${id}` } export function getNotAttachReason(buftype: string, enabled: number | undefined, size: number): string { if (!['', 'acwrite'].includes(buftype)) { return `not a normal buffer, buftype "${buftype}"` } if (enabled === 0) { return `b:coc_enabled = 0` } return `buffer size ${size} exceed coc.preferences.maxFileSize` } ================================================ FILE: src/model/download.ts ================================================ 'use strict' import http, { IncomingHttpHeaders, IncomingMessage } from 'http' import { URL } from 'url' import { v1 as uuidv1 } from 'uuid' import { createLogger } from '../logger' import { crypto, fs, path } from '../util/node' import { CancellationToken } from '../util/protocol' import { FetchOptions, getRequestModule, resolveRequestOptions, timeout, toURL } from './fetch' const logger = createLogger('model-download') export interface DownloadOptions extends Omit { /** * Folder that contains downloaded file or extracted files by untar or unzip */ dest: string /** * algorithm for check etag. */ etagAlgorithm?: string /** * Remove the specified number of leading path elements for *untar* only, default to `1`. */ strip?: number /** * If true, use untar for `.tar.gz` filename */ extract?: boolean | 'untar' | 'unzip' onProgress?: (percent: string) => void agent?: http.Agent } export function getEtag(headers: IncomingHttpHeaders): string | undefined { let header = headers['etag'] if (typeof header !== 'string') return undefined header = header.replace(/^W\//, '') if (!header.startsWith('"') || !header.endsWith('"')) return undefined return header.slice(1, -1) } export function getExtname(dispositionHeader: string): string | undefined { const contentDisposition = require('content-disposition') let disposition = contentDisposition.parse(dispositionHeader) let filename = disposition.parameters.filename if (filename) return path.extname(filename) return undefined } /** * Download file from url, with optional untar/unzip support. * @param {string} url * @param {DownloadOptions} options contains dest folder and optional onProgress callback */ export default function download(urlInput: string | URL, options: DownloadOptions, token?: CancellationToken, obj: any = {}): Promise { let url = toURL(urlInput) let { etagAlgorithm } = options let { dest, onProgress, extract } = options if (!dest || !path.isAbsolute(dest)) { throw new Error(`Invalid dest path: ${dest}`) } if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }) } else { let stat = fs.statSync(dest) if (stat && !stat.isDirectory()) { throw new Error(`${dest} exists, but not directory!`) } } let mod = getRequestModule(url) let opts = resolveRequestOptions(url, options) if (!opts.agent && options.agent) opts.agent = options.agent let extname = path.extname(url.pathname) return new Promise((resolve, reject) => { if (token) { let disposable = token.onCancellationRequested(() => { disposable.dispose() req.destroy(new Error('request aborted')) }) } let timer: NodeJS.Timeout const req = mod.request(opts, (res: IncomingMessage) => { if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) { let headers = res.headers let dispositionHeader = headers['content-disposition'] let etag = getEtag(headers) let checkEtag = etag && typeof etagAlgorithm === 'string' if (!extname && dispositionHeader) { extname = getExtname(dispositionHeader) } if (extract === true) { if (extname === '.zip' || headers['content-type'] == 'application/zip') { extract = 'unzip' } else if (extname == '.tgz') { extract = 'untar' } else { reject(new Error(`Unable to detect extract method for ${url}`)) return } } let total = Number(headers['content-length']) let hasTotal = !isNaN(total) let cur = 0 res.on('error', err => { reject(new Error(`Unable to connect ${url}: ${err.message}`)) }) let hash = checkEtag ? crypto.createHash(etagAlgorithm) : undefined res.on('data', chunk => { cur += chunk.length if (hash) hash.update(chunk) if (hasTotal) { let percent = (cur / total * 100).toFixed(1) if (typeof onProgress === 'function') { onProgress(percent) } else { logger.info(`Download ${url} progress ${percent}%`) } } }) res.on('end', () => { clearTimeout(timer) timer = undefined logger.info('Download completed:', url) }) let stream: any if (extract === 'untar') { const tar = require('tar') stream = res.pipe(tar.x({ strip: options.strip ?? 1, C: dest })) } else if (extract === 'unzip') { const unzip = require('unzip-stream') stream = res.pipe(unzip.Extract({ path: dest })) } else { dest = path.join(dest, `${uuidv1()}${extname}`) stream = res.pipe(fs.createWriteStream(dest)) } stream.on('finish', () => { if (hash) { if (hash.digest('hex') !== etag) { reject(new Error(`Etag check failed by ${etagAlgorithm}, content not match.`)) return } } logger.info(`Downloaded ${url} => ${dest}`) setTimeout(() => { resolve(dest) }, 100) }) stream.on('error', reject) } else { reject(new Error(`Invalid response from ${url}: ${res.statusCode}`)) } }) obj.req = req req.on('error', e => { // Possible succeed proxy request with ECONNRESET error on node > 14 if (e['code'] == 'ECONNRESET') { timer = setTimeout(() => { reject(e) }, timeout) } else { clearTimeout(timer) if (opts.agent && opts.agent.proxy) { reject(new Error(`Request failed using proxy ${opts.agent.proxy.host}: ${e.message}`)) return } reject(e) } }) req.on('timeout', () => { req.destroy(new Error(`request timeout after ${options.timeout}ms`)) }) if (typeof options.timeout === 'number' && options.timeout) { req.setTimeout(options.timeout) } req.end() }) } ================================================ FILE: src/model/editInspect.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { ChangeAnnotation, CreateFile, DeleteFile, Position, RenameFile, SnippetTextEdit, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import type { LinesChange } from '../core/files' import type Keymaps from '../core/keymaps' import events from '../events' import { DocumentChange } from '../types' import { disposeAll } from '../util' import { toArray } from '../util/array' import { isParentFolder } from '../util/fs' import { fastDiff, path } from '../util/node' import { Disposable } from '../util/protocol' import { getAnnotationKey, getPositionFromEdits, mergeSortEdits } from '../util/textedit' import Highlighter from './highlighter' export type RecoverFunc = () => Promise | void export interface EditState { edit: WorkspaceEdit changes: { [uri: string]: LinesChange } recovers: RecoverFunc[] applied: boolean } export interface ChangedFileItem { index: number filepath: string lnum?: number } let global_id = 0 export default class EditInspect { private disposables: Disposable[] = [] private bufnr: number private items: ChangedFileItem[] = [] private renameMap: Map = new Map() constructor(private nvim: Neovim, private keymaps: Keymaps) { events.on('BufUnload', bufnr => { if (bufnr == this.bufnr) this.dispose() }, null, this.disposables) } private addFile(filepath: string, highlighter: Highlighter, lnum?: number): void { this.items.push({ index: highlighter.length, filepath, lnum }) } public async show(state: EditState): Promise { let { nvim } = this let id = global_id++ nvim.pauseNotification() nvim.command(`tabe +setl\\ buftype=nofile CocWorkspaceEdit${id}`, true) nvim.command(`setl bufhidden=wipe nolist`, true) nvim.command('setl nobuflisted wrap undolevels=-1 filetype=cocedits noswapfile', true) await nvim.resumeNotification(true) let buffer = await nvim.buffer let cwd = await nvim.call('getcwd') as string this.bufnr = buffer.id const relpath = (uri: string): string => { let fsPath = URI.parse(uri).fsPath return isParentFolder(cwd, fsPath, true) ? path.relative(cwd, fsPath) : fsPath } const absPath = filepath => { return path.isAbsolute(filepath) ? filepath : path.join(cwd, filepath) } let highlighter = new Highlighter() let changes = toArray(state.edit.documentChanges) let map = grouByAnnotation(changes, state.edit.changeAnnotations ?? {}) for (let [label, changes] of map.entries()) { if (label) { highlighter.addLine(label, 'MoreMsg') highlighter.addLine('') } for (let change of changes) { if (TextDocumentEdit.is(change)) { let linesChange = state.changes[change.textDocument.uri] let fsPath = relpath(change.textDocument.uri) highlighter.addTexts([ { text: 'Change', hlGroup: 'Title' }, { text: ' ' }, { text: fsPath, hlGroup: 'Directory' }, { text: `:${linesChange.lnum}`, hlGroup: 'LineNr' }, ]) this.addFile(fsPath, highlighter, linesChange.lnum) highlighter.addLine('') this.addChangedLines(highlighter, linesChange, fsPath, linesChange.lnum) highlighter.addLine('') } else if (CreateFile.is(change) || DeleteFile.is(change)) { let title = DeleteFile.is(change) ? 'Delete' : 'Create' let fsPath = relpath(change.uri) highlighter.addTexts([ { text: title, hlGroup: 'Title' }, { text: ' ' }, { text: fsPath, hlGroup: 'Directory' } ]) this.addFile(fsPath, highlighter) highlighter.addLine('') } else if (RenameFile.is(change)) { let oldPath = relpath(change.oldUri) let newPath = relpath(change.newUri) highlighter.addTexts([ { text: 'Rename', hlGroup: 'Title' }, { text: ' ' }, { text: oldPath, hlGroup: 'Directory' }, { text: '->', hlGroup: 'Comment' }, { text: newPath, hlGroup: 'Directory' } ]) this.renameMap.set(oldPath, newPath) this.addFile(newPath, highlighter) highlighter.addLine('') } } } nvim.pauseNotification() highlighter.render(buffer) buffer.setOption('modifiable', false, true) await nvim.resumeNotification(true) this.disposables.push(this.keymaps.registerLocalKeymap(buffer.id, 'n', '', async () => { let lnum = await nvim.call('line', '.') as number let col = await nvim.call('col', '.') as number let find: ChangedFileItem for (let i = this.items.length - 1; i >= 0; i--) { let item = this.items[i] if (lnum >= item.index) { find = item break } } if (!find) return let uri = URI.file(absPath(find.filepath)).toString() let filepath = this.renameMap.has(find.filepath) ? this.renameMap.get(find.filepath) : find.filepath await nvim.call('coc#util#open_file', ['tab drop', absPath(filepath)]) let documentChanges = toArray(state.edit.documentChanges) let change = documentChanges.find(o => TextDocumentEdit.is(o) && o.textDocument.uri == uri) as TextDocumentEdit let originLine = getOriginalLine(find, change) if (originLine !== undefined) await nvim.call('cursor', [originLine, col]) nvim.redrawVim() }, true)) this.disposables.push(this.keymaps.registerLocalKeymap(buffer.id, 'n', '', async () => { nvim.command('bwipeout!', true) }, true)) } public addChangedLines(highlighter: Highlighter, linesChange: LinesChange, fsPath: string, lnum: number): void { let diffs = fastDiff(linesChange.oldLines.join('\n'), linesChange.newLines.join('\n')) for (let i = 0; i < diffs.length; i++) { let diff = diffs[i] if (diff[0] == fastDiff.EQUAL) { let text = diff[1] if (!text.includes('\n')) { highlighter.addText(text) } else { let parts = text.split('\n') highlighter.addText(parts[0]) let curr = lnum + parts.length - 1 highlighter.addLine('') highlighter.addTexts([ { text: 'Change', hlGroup: 'Title' }, { text: ' ' }, { text: fsPath, hlGroup: 'Directory' }, { text: `:${curr}`, hlGroup: 'LineNr' }, ]) this.addFile(fsPath, highlighter, curr) highlighter.addLine('') let last = parts[parts.length - 1] highlighter.addText(last) } lnum += text.split('\n').length - 1 } else if (diff[0] == fastDiff.DELETE) { lnum += diff[1].split('\n').length - 1 highlighter.addText(diff[1], 'DiffDelete') } else { highlighter.addText(diff[1], 'DiffAdd') } } } public dispose(): void { disposeAll(this.disposables) } } export function getOriginalLine(item: ChangedFileItem, change: TextDocumentEdit | undefined): number | undefined { if (typeof item.lnum !== 'number') return undefined let lnum = item.lnum if (change) { // Use snippet value as text should be fine to get the line number. let edits: TextEdit[] = change.edits.map(o => SnippetTextEdit.is(o) ? { range: o.range, newText: o.snippet.value } : o) edits = mergeSortEdits(edits) let pos = getPositionFromEdits(Position.create(lnum - 1, 0), edits) lnum = pos.line + 1 } return lnum } function grouByAnnotation(changes: DocumentChange[], annotations: { [id: string]: ChangeAnnotation }): Map { let map: Map = new Map() for (let change of changes) { let id = getAnnotationKey(change) ?? null let key = id ? annotations[id]?.label : null let arr = map.get(key) if (arr) { arr.push(change) } else { map.set(key, [change]) } } return map } ================================================ FILE: src/model/fetch.ts ================================================ 'use strict' import decompressResponse from 'decompress-response' import { http, https } from 'follow-redirects' import { HttpProxyAgent } from 'http-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent' import { ParsedUrlQueryInput, stringify } from 'querystring' import { Readable } from 'stream' import { URL } from 'url' import { createLogger } from '../logger' import { getConditionValue } from '../util' import { CancellationError } from '../util/errors' import { objectLiteral } from '../util/is' import { fs } from '../util/node' import { CancellationToken } from '../util/protocol' import { toText } from '../util/string' import workspace from '../workspace' import { Agent } from 'node:http' const logger = createLogger('model-fetch') export const timeout = getConditionValue(500, 50) export type ResponseResult = string | Buffer | { [name: string]: any } export interface ProxyOptions { proxy: string proxyStrictSSL?: boolean proxyAuthorization?: string | null proxyCA?: string | null } export interface FetchOptions { /** * Default to 'GET' */ method?: string /** * Default no timeout */ timeout?: number /** * Always return buffer instead of parsed response. */ buffer?: boolean /** * - 'string' for text response content * - 'object' for json response content * - 'buffer' for response not text or json */ data?: string | { [key: string]: any } | Buffer /** * Plain object added as query of url */ query?: ParsedUrlQueryInput headers?: any /** * User for http basic auth, should use with password */ user?: string /** * Password for http basic auth, should use with user */ password?: string } export function getRequestModule(url: URL): typeof http | typeof https { return url.protocol === 'https:' ? https : http } export function getText(data: any): string | Buffer { if (typeof data === 'string' || Buffer.isBuffer(data)) return data return JSON.stringify(data) } export function toURL(urlInput: string | URL): URL { if (urlInput instanceof URL) return urlInput let url = new URL(urlInput) if (!['https:', 'http:'].includes(url.protocol)) throw new Error(`Not valid protocol with ${urlInput}, should be http: or https:`) return url } export function toPort(port: number | string | undefined, protocol: string): number { if (port) { port = typeof port === 'number' ? port : parseInt(port, 10) if (!isNaN(port)) return port } return protocol.startsWith('https') ? 443 : 80 } export function getDataType(data: any): string { if (data === null) return 'null' if (data === undefined) return 'undefined' if (typeof data == 'string') return 'string' if (Buffer.isBuffer(data)) return 'buffer' if (Array.isArray(data) || objectLiteral(data)) return 'object' return 'unknown' } export function getSystemProxyURI(endpoint: URL, env = process.env): string | null { let noProxy = env.NO_PROXY ?? env.no_proxy if (noProxy === '*') { return null } if (noProxy) { // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' const hostname = endpoint.hostname.replace(/^\.*/, '.').toLowerCase() const port = toPort(endpoint.port, endpoint.protocol).toString() const noProxyList = noProxy.split(',') for (let i = 0, len = noProxyList.length; i < len; i++) { let noProxyItem = noProxyList[i].trim().toLowerCase() // no_proxy can be granular at the port level, which complicates things a bit. if (noProxyItem.includes(':')) { let noProxyItemParts = noProxyItem.split(':', 2) let noProxyHost = noProxyItemParts[0].replace(/^\.*/, '.') let noProxyPort = noProxyItemParts[1] if (port == noProxyPort && hostname.endsWith(noProxyHost)) { return null } } else { noProxyItem = noProxyItem.replace(/^\.*/, '.') if (hostname.endsWith(noProxyItem)) { return null } } } } let proxyUri: string | null if (endpoint.protocol === 'http:') { proxyUri = env.HTTP_PROXY || env.http_proxy || null } else { proxyUri = env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || null } return proxyUri } export function getAgent(endpoint: URL, options: ProxyOptions): Agent { let proxy = options.proxy || getSystemProxyURI(endpoint) if (proxy) { let proxyURL: URL try { proxyURL = new URL(proxy) if (!/^https?:$/.test(proxyURL.protocol)) return null } catch (e) { return null } let rejectUnauthorized = typeof options.proxyStrictSSL === 'boolean' ? options.proxyStrictSSL : true logger.info(`Using proxy ${proxy} from ${options.proxy ? 'configuration' : 'system environment'} for ${endpoint.hostname}:`) const agentOptions = { rejectUnauthorized } return endpoint.protocol === 'http:' ? new HttpProxyAgent(proxyURL, agentOptions) : new HttpsProxyAgent(proxyURL, agentOptions) } return null } export function resolveRequestOptions(url: URL, options: FetchOptions): any { let config = workspace.getConfiguration('http', null) let dataType = getDataType(options.data) let proxyOptions: ProxyOptions = { proxy: config.get('proxy', ''), proxyStrictSSL: config.get('proxyStrictSSL', true), proxyAuthorization: config.get('proxyAuthorization', null), proxyCA: config.get('proxyCA', null) } if (options.query && !url.search) { url.search = `?${stringify(options.query)}` } let agent = getAgent(url, proxyOptions) let opts: any = { method: options.method ?? 'GET', hostname: url.hostname, port: toPort(url.port, url.protocol), path: url.pathname + url.search, agent, rejectUnauthorized: proxyOptions.proxyStrictSSL, maxRedirects: 3, headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)', 'Accept-Encoding': 'gzip, deflate', ...(options.headers ?? {}) } } if (dataType == 'object') { opts.headers['Content-Type'] = 'application/json' } else if (dataType == 'string') { opts.headers['Content-Type'] = 'text/plain' } if (proxyOptions.proxyAuthorization) opts.headers['Proxy-Authorization'] = proxyOptions.proxyAuthorization if (proxyOptions.proxyCA) opts.ca = fs.readFileSync(proxyOptions.proxyCA) if (options.user) opts.auth = options.user + ':' + (toText(options.password)) if (url.username) opts.auth = url.username + ':' + (toText(url.password)) if (options.timeout) opts.timeout = options.timeout if (options.buffer) opts.buffer = true return opts } export function request(url: URL, data: any, opts: any, token?: CancellationToken, obj: any = {}): Promise { let mod = getRequestModule(url) return new Promise((resolve, reject) => { if (token) { let disposable = token.onCancellationRequested(() => { disposable.dispose() req.destroy(new CancellationError()) }) } let timer: NodeJS.Timeout const req = mod.request(opts, res => { let readable: Readable = res if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) { let headers = res.headers let chunks: Buffer[] = [] let contentType: string = toText(headers['content-type']) readable = decompressResponse(res) readable.on('data', chunk => { chunks.push(chunk) }) readable.on('end', () => { clearTimeout(timer) let buf = Buffer.concat(chunks) if (!opts.buffer && (contentType.startsWith('application/json') || contentType.startsWith('text/'))) { let ms = contentType.match(/charset=(\S+)/) let encoding = ms ? ms[1] : 'utf8' let rawData = buf.toString(encoding as BufferEncoding) if (!contentType.includes('application/json')) { resolve(rawData) } else { try { const parsedData = JSON.parse(rawData) resolve(parsedData) } catch (e) { reject(new Error(`Parse response error: ${e}`)) } } } else { resolve(buf) } }) readable.on('error', err => { reject(new Error(`Connection error to ${url}: ${err.message}`)) }) } else { reject(new Error(`Bad response from ${url}: ${res.statusCode}`)) } }) obj.req = req req.on('error', e => { // Possible succeed proxy request with ECONNRESET error on node > 14 if (e['code'] == 'ECONNRESET') { timer = setTimeout(() => { reject(e) }, timeout) } else { reject(e) } }) req.on('timeout', () => { req.destroy(new Error(`Request timeout after ${opts.timeout}ms`)) }) if (data) req.write(getText(data)) if (opts.timeout) req.setTimeout(opts.timeout) req.end() }) } /** * Send request to server for response, supports: * * - Send json data and parse json response. * - Throw error for failed response statusCode. * - Timeout support (no timeout by default). * - Send buffer (as data) and receive data (as response). * - Proxy support from user configuration & environment. * - Redirect support, limited to 3. * - Support of gzip & deflate response content. */ export default function fetch(urlInput: string | URL, options: FetchOptions = {}, token?: CancellationToken): Promise { let url = toURL(urlInput) let opts = resolveRequestOptions(url, options) return request(url, options.data, opts, token).catch(err => { logger.error(`Fetch error for ${url}:`, opts, err) if (opts.agent && opts.agent.proxy) { let { proxy } = opts.agent throw new Error(`Request failed using proxy ${proxy.host}: ${err.message}`) } else { throw err } }) } ================================================ FILE: src/model/floatFactory.ts ================================================ 'use strict' import { Buffer, Neovim, Window } from '@chemzqm/neovim' import events, { BufEvents } from '../events' import { parseDocuments } from '../markdown' import { Documentation, FloatConfig } from '../types' import { disposeAll, getConditionValue } from '../util' import { isFalsyOrEmpty } from '../util/array' import { isVim } from '../util/constants' import { Mutex } from '../util/mutex' import { debounce } from '../util/node' import { equals } from '../util/object' import { Disposable } from '../util/protocol' const debounceTime = getConditionValue(100, 10) export interface WindowConfig { width: number height: number col: number row: number relative: 'cursor' | 'win' | 'editor' style?: string cursorline?: number title?: string border?: number[] autohide?: number close?: number } export interface FloatWinConfig extends FloatConfig { breaks?: boolean preferTop?: boolean autoHide?: boolean offsetX?: number cursorline?: boolean modes?: string[] excludeImages?: boolean position?: "fixed" | "auto" top?: number bottom?: number left?: number right?: number } /** * Float window/popup factory for create float/popup around current cursor. */ export default class FloatFactoryImpl implements Disposable { private winid = 0 private _bufnr = 0 private closeTs: number private targetBufnr: number private mutex: Mutex = new Mutex() private disposables: Disposable[] = [] private cursor: [number, number] // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type private onCursorMoved: Function & { clear(): void } constructor(private nvim: Neovim) { this.onCursorMoved = debounce(this._onCursorMoved.bind(this), debounceTime) } private bindEvents(autoHide: boolean, alignTop: boolean): void { let eventNames: BufEvents[] = ['InsertLeave', 'InsertEnter', 'BufEnter'] for (let ev of eventNames) { events.on(ev, bufnr => { if (bufnr == this._bufnr) return this.close() }, null, this.disposables) } events.on('MenuPopupChanged', () => { // avoid intersect with pum if (events.pumAlignTop == alignTop) { this.close() } }, null, this.disposables) this.disposables.push(Disposable.create(() => { this.onCursorMoved.clear() })) events.on('CursorMoved', this.onCursorMoved.bind(this, autoHide), this, this.disposables) events.on('CursorMovedI', this.onCursorMoved.bind(this, autoHide), this, this.disposables) } public unbind(): void { if (this.disposables.length) { disposeAll(this.disposables) this.disposables = [] } } public _onCursorMoved(autoHide: boolean, bufnr: number, cursor: [number, number]): void { if (bufnr == this._bufnr) return if (bufnr == this.targetBufnr && equals(cursor, this.cursor)) { // cursor not moved return } if (bufnr != this.targetBufnr || !events.insertMode || autoHide) { this.close() return } } /** * Create float window/popup at cursor position. * @deprecated use show method instead */ public async create(docs: Documentation[], _allowSelection = false, offsetX = 0): Promise { await this.show(docs, { offsetX }) } /** * Show documentations in float window/popup around cursor. * Window and buffer are reused when possible. * Window is closed automatically on change buffer, InsertEnter, CursorMoved and CursorMovedI. * @param docs List of documentations. * @param config Configuration for floating window/popup. */ public async show(docs: Documentation[], config: FloatWinConfig = {}): Promise { if (docs.length == 0 || docs.every(doc => doc.content.length == 0)) { this.close() return } let curr = Date.now() let release = await this.mutex.acquire() try { await this.createPopup(docs, config, curr) release() } catch (e) { this.nvim.echoError(e) release() } } private async createPopup(docs: Documentation[], opts: FloatWinConfig, timestamp: number): Promise { docs = docs.filter(o => o.content.trim().length > 0) let { lines, codes, highlights } = parseDocuments(docs, { excludeImages: opts.excludeImages, breaks: opts.breaks }) let config: any = { codes, highlights, pumAlignTop: events.pumAlignTop, preferTop: typeof opts.preferTop === 'boolean' ? opts.preferTop : false, offsetX: opts.offsetX || 0, title: opts.title || '', close: opts.close ? 1 : 0, rounded: opts.rounded ? 1 : 0, modes: opts.modes || ['n', 'i', 'ic', 's'], relative: opts.position === 'fixed' ? 'editor' : 'cursor' } if (!isVim) { if (typeof opts.winblend === 'number') config.winblend = opts.winblend if (opts.focusable != null) config.focusable = opts.focusable ? 1 : 0 if (opts.shadow) config.shadow = 1 } if (opts.maxHeight) config.maxHeight = opts.maxHeight if (opts.maxWidth) config.maxWidth = opts.maxWidth if (opts.border === true) { config.border = [1, 1, 1, 1] } else if (Array.isArray(opts.border) && !opts.border.every(o => o == 0)) { config.border = opts.border.slice(0, 4) config.rounded = opts.rounded ? 1 : 0 } if (opts.highlight) config.highlight = opts.highlight if (opts.borderhighlight) config.borderhighlight = opts.borderhighlight if (opts.cursorline) config.cursorline = 1 for (let key of ['top', 'left', 'bottom', 'right']) { if (typeof opts[key] === 'number' && opts[key] >= 0) { config[key] = opts[key] } } let autoHide = opts.autoHide === false ? false : true if (autoHide) config.autohide = 1 this.unbind() let arr = await this.nvim.call('coc#dialog#create_cursor_float', [this.winid, this._bufnr, lines, config]) as [number, [number, number], number, number, number] if (isFalsyOrEmpty(arr) || this.closeTs > timestamp) { let winid = arr && arr.length > 0 ? arr[2] : this.winid if (winid) { this.winid = 0 this.nvim.call('coc#float#close', [winid], true) this.nvim.redrawVim() } return } let [targetBufnr, cursor, winid, bufnr, alignTop] = arr this.winid = winid this._bufnr = bufnr this.targetBufnr = targetBufnr this.cursor = cursor this.bindEvents(autoHide, alignTop == 1) } /** * Close float window */ public close(): void { let { winid, nvim } = this this.closeTs = Date.now() this.unbind() if (winid) { this.winid = 0 nvim.call('coc#float#close', [winid], true) nvim.redrawVim() } } public checkRetrigger(bufnr: number): boolean { if (this.winid && this.targetBufnr == bufnr) return true return false } public get bufnr(): number { return this._bufnr } public get buffer(): Buffer | null { return this.bufnr ? this.nvim.createBuffer(this.bufnr) : null } public get window(): Window | null { return this.winid ? this.nvim.createWindow(this.winid) : null } public async activated(): Promise { if (!this.winid) return false return await this.nvim.call('coc#float#valid', [this.winid]) != 0 } public dispose(): void { this.cursor = undefined this.onCursorMoved.clear() this.close() } } ================================================ FILE: src/model/fuzzyMatch.ts ================================================ import { AnsiHighlight } from '../types' import { pluginRoot } from '../util/constants' import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions, FuzzyScorer } from '../util/filter' import { fs, path, promisify } from '../util/node' import { bytes } from '../util/string' export interface FuzzyWasi { fuzzyMatch: (textPtr: number, patternPtr: number, resultPtr: number, matchSeq: 0 | 1) => number malloc: (size: number) => number free: (ptr: number) => void memory: { buffer: ArrayBuffer } } export type FuzzyKind = 'normal' | 'aggressive' | 'any' export type ScoreFunction = (word: string, wordPos?: number) => FuzzyScore | undefined export interface MatchResult { score: number positions: Uint32Array } export interface MatchHighlights { score: number highlights: AnsiHighlight[] } const wasmFile = path.join(pluginRoot, 'bin/fuzzy.wasm') export async function initFuzzyWasm(): Promise { const buffer = await promisify(fs.readFile)(wasmFile) const res = await global.WebAssembly.instantiate(buffer, { env: {} }) return res.instance.exports as FuzzyWasi } /** * Convert FuzzyScore to highlight byte spans. */ export function toSpans(label: string, score: FuzzyScore): [number, number][] { let res: [number, number][] = [] for (let span of matchSpansReverse(label, score, 2)) { res.push(span) } return res } /** * Convert to spans from reversed list of utf16 code unit numbers * Positions should be sorted numbers from big to small */ export function* matchSpansReverse(text: string, positions: ArrayLike, endIndex = 0, max = Number.MAX_SAFE_INTEGER): Iterable<[number, number]> { let len = positions.length if (len <= endIndex) return let byteIndex = bytes(text, Math.min(positions[endIndex] + 1, max)) let start: number | undefined let prev: number | undefined for (let i = len - 1; i >= endIndex; i--) { let curr = positions[i] if (curr >= max) { if (start != null) yield [byteIndex(start), byteIndex(prev + 1)] break } if (prev != undefined) { let d = curr - prev if (d == 1) { prev = curr } else if (d > 1) { yield [byteIndex(start), byteIndex(prev + 1)] start = curr } else { // invalid number yield [byteIndex(start), byteIndex(prev + 1)] break } } else { start = curr } prev = curr if (i == endIndex) { yield [byteIndex(start), byteIndex(prev + 1)] } } } function* matchSpans(text: string, positions: ArrayLike, max?: number): Iterable<[number, number]> { max = max ? Math.min(max, text.length) : text.length let byteIndex = bytes(text, Math.min(text.length, 4096)) let start: number | undefined let prev: number | undefined let len = positions.length for (let i = 0; i < len; i++) { let curr = positions[i] if (curr >= max) { if (start != null) yield [byteIndex(start), byteIndex(prev + 1)] break } if (prev != undefined) { let d = curr - prev if (d == 1) { prev = curr } else if (d > 1) { yield [byteIndex(start), byteIndex(prev + 1)] start = curr } else { // invalid number yield [byteIndex(start), byteIndex(prev + 1)] break } } else { start = curr } prev = curr if (i == len - 1) { yield [byteIndex(start), byteIndex(prev + 1)] } } } export class FuzzyMatch { private contentPtr: number | undefined private patternPtr: number | undefined private resultPtr: number | undefined private patternLength = 0 private matchSeq = false private sizes: number[] = [2048, 1024, 1024] constructor(private exports: FuzzyWasi) { } /** * Match character positions to column spans. */ public matchSpans(text: string, positions: ArrayLike, max?: number): Iterable<[number, number]> { return matchSpans(text, positions, max) } /** * Create 0 index byte spans from matched text and FuzzyScore for highlights */ public matchScoreSpans(text: string, score: FuzzyScore): Iterable<[number, number]> { return matchSpansReverse(text, score, 2) } /** * Create a score function */ public createScoreFunction(pattern: string, patternPos: number, options?: FuzzyScoreOptions, kind?: FuzzyKind): ScoreFunction { let lowPattern = pattern.toLowerCase() let fn: FuzzyScorer if (kind === 'any') { fn = anyScore } else if (kind === 'aggressive') { fn = fuzzyScoreGracefulAggressive } else { fn = fuzzyScore } return (word: string, wordPos = 0): FuzzyScore | undefined => { return fn(pattern, lowPattern, patternPos, word, word.toLowerCase(), wordPos, options) } } public getSizes(): number[] { return this.sizes } public setPattern(pattern: string, matchSeq = false): void { // Can't handle length > 256 if (pattern.length > 256) pattern = pattern.slice(0, 256) this.matchSeq = matchSeq this.patternLength = matchSeq ? pattern.length : pattern.replace(/(\s|\t)/g, '').length if (this.patternPtr == null) { let { malloc } = this.exports let { sizes } = this this.contentPtr = malloc(sizes[0]) this.patternPtr = malloc(sizes[1]) this.resultPtr = malloc(sizes[2]) } let buf = Buffer.from(pattern, 'utf8') let len = buf.length let bytes = new Uint8Array(this.exports.memory.buffer, this.patternPtr, len + 1) bytes.set(buf) bytes[len] = 0 } private changeContent(text: string): void { let { sizes } = this if (text.length > 4096) text = text.slice(0, 4096) let buf = Buffer.from(text, 'utf8') let len = buf.length if (len > sizes[0]) { let { malloc, free } = this.exports free(this.contentPtr) let byteLength = len + 1 this.contentPtr = malloc(byteLength) sizes[0] = byteLength } let bytes = new Uint8Array(this.exports.memory.buffer, this.contentPtr, len + 1) bytes.set(buf) bytes[len] = 0 } public match(text: string): MatchResult | undefined { if (this.patternPtr == null) throw new Error('setPattern not called before match') if (this.patternLength === 0) return { score: 100, positions: new Uint32Array() } this.changeContent(text) let { fuzzyMatch, memory } = this.exports let { resultPtr } = this let score = fuzzyMatch(this.contentPtr, this.patternPtr, resultPtr, this.matchSeq ? 1 : 0) if (!score) return undefined const u32 = new Uint32Array(memory.buffer, resultPtr, this.patternLength) return { score, positions: u32.slice() } } public matchHighlights(text: string, hlGroup: string): MatchHighlights | undefined { let res = this.match(text) if (!res) return undefined let highlights: AnsiHighlight[] = [] for (let span of this.matchSpans(text, res.positions)) { highlights.push({ span, hlGroup }) } return { score: res.score, highlights } } public free(): void { let ptrs = [this.contentPtr, this.patternPtr, this.resultPtr] let { free } = this.exports ptrs.forEach(p => { if (p != null) free(p) }) this.contentPtr = this.patternPtr = this.resultPtr = undefined } } ================================================ FILE: src/model/highlighter.ts ================================================ 'use strict' import { Buffer } from '@chemzqm/neovim' import { parseAnsiHighlights } from '../util/ansiparse' import { byteLength } from '../util/string' import { HighlightItem } from '../types' export interface TextItem { text: string hlGroup?: string } /** * Build highlights, with lines and highlights */ export default class Highlighter { private lines: string[] = [] private _highlights: HighlightItem[] = [] public addLine(line: string, hlGroup?: string): void { if (line.includes('\n')) { for (let content of line.split(/\r?\n/)) { this.addLine(content, hlGroup) } return } if (hlGroup) { this._highlights.push({ lnum: this.lines.length, colStart: line.match(/^\s*/)[0].length, colEnd: byteLength(line), hlGroup }) } // '\x1b' if (line.includes('\x1b')) { let res = parseAnsiHighlights(line) for (let hl of res.highlights) { let { span, hlGroup } = hl this._highlights.push({ lnum: this.lines.length, colStart: span[0], colEnd: span[1], hlGroup }) } this.lines.push(res.line) } else { this.lines.push(line) } } public addLines(lines: string[]): void { this.lines.push(...lines) } /** * Add texts to new Lines */ public addTexts(items: TextItem[]): void { let len = this.lines.length let text = '' for (let item of items) { let colStart = byteLength(text) if (item.hlGroup) { this._highlights.push({ lnum: len, colStart, colEnd: colStart + byteLength(item.text), hlGroup: item.hlGroup }) } text += item.text } this.lines.push(text) } public addText(text: string, hlGroup?: string): void { if (!text) return let { lines } = this let pre = lines[lines.length - 1] || '' if (text.includes('\n')) { let parts = text.split('\n') this.addText(parts[0], hlGroup) for (let line of parts.slice(1)) { this.addLine(line, hlGroup) } return } if (hlGroup) { let colStart = byteLength(pre) this._highlights.push({ lnum: lines.length ? lines.length - 1 : 0, colStart, colEnd: colStart + byteLength(text), hlGroup }) } if (lines.length) { lines[lines.length - 1] = `${pre}${text}` } else { lines.push(text) } } public get length(): number { return this.lines.length } public getline(line: number): string { return this.lines[line] || '' } public get highlights(): ReadonlyArray { return this._highlights } public get content(): string { return this.lines.join('\n') } // default to replace public render(buffer: Buffer, start = 0, end = -1): void { buffer.setLines(this.lines, { start, end, strictIndexing: false }, true) for (let item of this._highlights) { // eslint-disable-next-line @typescript-eslint/no-floating-promises buffer.addHighlight({ hlGroup: item.hlGroup, colStart: item.colStart, colEnd: item.colEnd, line: start + item.lnum, srcId: -1 }) } } } ================================================ FILE: src/model/input.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import { disposeAll } from '../util' import { isVim } from '../util/constants' import { omitUndefined } from '../util/object' import { Disposable, Emitter, Event } from '../util/protocol' import { toText } from '../util/string' export interface InputPreference { placeHolder?: string position?: 'cursor' | 'center' marginTop?: number border?: [0 | 1, 0 | 1, 0 | 1, 0 | 1] rounded?: boolean minWidth?: number maxWidth?: number highlight?: string borderhighlight?: string quickpick?: string[] /** * map list key-mappings */ list?: boolean } export interface Dimension { width: number height: number row: number col: number } type RequestResult = [number, number, [number, number, number, number]] export default class InputBox implements Disposable { private disposables: Disposable[] = [] private _winid: number | undefined private _bufnr: number | undefined private _input: string private accepted = false private _disposed = false public title: string public loading: boolean public value: string public borderhighlight: string // width, height, row, col private _dimension: [number, number, number, number] = [0, 0, 0, 0] private readonly _onDidFinish = new Emitter() private readonly _onDidChange = new Emitter() private clear = false public readonly onDidFinish: Event = this._onDidFinish.event public readonly onDidChange: Event = this._onDidChange.event constructor(private nvim: Neovim, defaultValue: string) { this._input = defaultValue this.disposables.push(this._onDidFinish) this.disposables.push(this._onDidChange) let _title: string | undefined Object.defineProperty(this, 'title', { set: (newTitle: string) => { _title = newTitle if (this._winid) nvim.call('coc#dialog#change_title', [this._winid, newTitle], true) }, get: () => { return _title } }) let _loading = false Object.defineProperty(this, 'loading', { set: (loading: boolean) => { _loading = loading if (this._winid) nvim.call('coc#dialog#change_loading', [this._winid, loading], true) }, get: () => { return _loading } }) let _borderhighlight: string Object.defineProperty(this, 'borderhighlight', { set: (borderhighlight: string) => { _borderhighlight = borderhighlight if (this._winid) nvim.call('coc#dialog#change_border_hl', [this._winid, borderhighlight], true) }, get: () => { return _borderhighlight } }) Object.defineProperty(this, 'value', { set: (value: string) => { value = toText(value) if (value !== this._input) { this.clearVirtualText() this._input = value this.nvim.call('coc#dialog#change_input_value', [this.winid, this.bufnr, value], true) this._onDidChange.fire(value) } }, get: () => { return this._input } }) events.on('BufWinLeave', bufnr => { if (bufnr == this._bufnr) { this.dispose() } }, null, this.disposables) events.on('PromptInsert', (value, bufnr) => { if (bufnr == this._bufnr) { this._input = value this.accepted = true this.dispose() } }, null, this.disposables) events.on('PromptExit', bufnr => { if (bufnr == this._bufnr) { this.dispose() } }, null, this.disposables) events.on('TextChangedI', (bufnr, info) => { if (bufnr == this._bufnr && this._input !== info.line) { this.clearVirtualText() this._input = info.line this._onDidChange.fire(info.line) } }, null, this.disposables) } private clearVirtualText(): void { if (this.clear && this.bufnr) { this.clear = false let buf = this.nvim.createBuffer(this.bufnr) buf.clearNamespace('input-box') } } public get dimension(): Dimension | undefined { let { _dimension } = this return { width: _dimension[0], height: _dimension[1], row: _dimension[2], col: _dimension[3] } } public get bufnr(): number | undefined { return this._bufnr } public get winid(): number | undefined { return this._winid } public async show(title: string, preferences: InputPreference): Promise { this.title = title this.borderhighlight = preferences.borderhighlight ?? 'CocFloatBorder' this.loading = false if (preferences.placeHolder && !this._input && !isVim) { this.clear = true } let res = await this.nvim.call('coc#dialog#create_prompt_win', [title, this._input, omitUndefined(preferences)]) as RequestResult if (!res) throw new Error('Unable to open input window') this._bufnr = res[0] this._winid = res[1] this._dimension = res[2] return true } public dispose(): void { if (this._disposed) return this._disposed = true this.nvim.call('coc#float#close', [this._winid ?? -1], true) if (isVim) this.nvim.command(`silent! bd! ${this._bufnr}`, true) this._onDidFinish.fire(this.accepted ? this._input : null) this._winid = undefined this._bufnr = undefined disposeAll(this.disposables) } } ================================================ FILE: src/model/line.ts ================================================ import { AnsiHighlight } from '../types' import { byteIndex, byteLength } from '../util/string' interface NestedHighlight { offset: number length: number hlGroup: string } /** * Build line with content and highlights. */ export default class LineBuilder { private _label = '' private _len = 0 private _highlights: AnsiHighlight[] = [] constructor(private addSpace = false) { } public append(text: string, hlGroup?: string, nested?: NestedHighlight[]): void { if (text.length == 0) return let space = this._len > 0 && this.addSpace ? ' ' : '' let start = this._len + space.length this._label = this._label + space + text this._len = this._len + byteLength(text) + space.length if (hlGroup) { this._highlights.push({ hlGroup, span: [start, start + byteLength(text)] }) } if (nested) { for (let item of nested) { let s = start + byteIndex(text, item.offset) let e = start + byteIndex(text, item.offset + item.length) this._highlights.push({ hlGroup: item.hlGroup, span: [s, e] }) } } } public appendBuilder(builder: LineBuilder): void { let space = this._len > 0 && this.addSpace ? ' ' : '' let curr = this._len + space.length this._label = this._label + space + builder.label this._len = this._len + byteLength(builder.label) + space.length this._highlights.push(...builder.highlights.map(item => { return { hlGroup: item.hlGroup, span: item.span.map(v => { return curr + v }) as [number, number] } })) } public get label(): string { return this._label } public get highlights(): AnsiHighlight[] { return this._highlights } } ================================================ FILE: src/model/memos.ts ================================================ 'use strict' import { loadJson, writeJson } from '../util/fs' import { fs } from '../util/node' import { deepClone } from '../util/object' /** * A memento represents a storage utility. It can store and retrieve * values. */ export interface Memento { get(key: string): T | undefined get(key: string, defaultValue: T): T update(key: string, value: any): Promise } export default class Memos { constructor(private filepath: string) { if (!fs.existsSync(filepath)) { fs.writeFileSync(filepath, '{}', 'utf8') } } public merge(filepath: string): void { if (!fs.existsSync(filepath)) return let obj = loadJson(filepath) let current = loadJson(this.filepath) Object.assign(current, obj) writeJson(this.filepath, current) fs.unlinkSync(filepath) } private fetchContent(id: string, key: string): any { let res = loadJson(this.filepath) let obj = res[id] if (!obj) return undefined return obj[key] } private async update(id: string, key: string, value: any): Promise { let { filepath } = this let current = loadJson(filepath) current[id] = current[id] || {} if (value !== undefined) { current[id][key] = deepClone(value) } else { delete current[id][key] } writeJson(filepath, current) } public createMemento(id: string): Memento { return { get: (key: string, defaultValue?: T): T | undefined => { let res = this.fetchContent(id, key) return res === undefined ? defaultValue : res }, update: async (key: string, value: any): Promise => { await this.update(id, key, value) } } } } ================================================ FILE: src/model/menu.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import { CancellationToken, Disposable, Emitter, Event } from '../util/protocol' import events from '../events' import { HighlightItem } from '../types' import { disposeAll } from '../util' import { byteLength, isAlphabet } from '../util/string' import { DialogPreferences } from './dialog' import Popup from './popup' export interface MenuItem { text: string disabled?: boolean | { reason: string } } export interface MenuConfig { items: string[] | MenuItem[] title?: string content?: string shortcuts?: boolean position?: 'cursor' | 'center' borderhighlight?: string } export function isMenuItem(item: any): item is MenuItem { if (!item) return false return typeof item.text === 'string' } export function toIndexText(n: number): string { return n < 99 ? `${n + 1}. ` : ' ' } /** * Select single item from menu at cursor position. */ export default class Menu { private bufnr: number private win: Popup private currIndex = 0 private contentHeight = 0 private total: number private disposables: Disposable[] = [] private keyMappings: Map void> = new Map() private shortcutIndexes: Set = new Set() private _disposed = false private readonly _onDidClose = new Emitter() public readonly onDidClose: Event = this._onDidClose.event constructor(private nvim: Neovim, private config: MenuConfig, token?: CancellationToken) { this.total = config.items.length if (token) { token.onCancellationRequested(() => { this._onDidClose.fire(-1) this.dispose() }) } this.disposables.push(this._onDidClose) this.addKeymappings() } private attachEvents(): void { events.on('InputChar', this.onInputChar.bind(this), null, this.disposables) events.on('BufWinLeave', bufnr => { if (bufnr == this.bufnr) { this._onDidClose.fire(-1) this.dispose() } }, null, this.disposables) } private addKeymappings(): void { let { nvim } = this this.addKeys(['', ''], () => { this._onDidClose.fire(-1) this.dispose() }) this.addKeys(['\r', ''], () => { this.selectCurrent() }) let setCursorIndex = idx => { nvim.pauseNotification() this.setCursor(idx + this.contentHeight) this.win.refreshScrollbar() nvim.command('redraw', true) nvim.resumeNotification(false, true) } this.addKeys('', async () => { await this.win.scrollForward() }) this.addKeys('', async () => { await this.win.scrollBackward() }) this.addKeys(['j', '', '', '', ''], () => { // next let idx = this.currIndex == this.total - 1 ? 0 : this.currIndex + 1 setCursorIndex(idx) }) this.addKeys(['k', '', '', '', ''], () => { // previous let idx = this.currIndex == 0 ? this.total - 1 : this.currIndex - 1 setCursorIndex(idx) }) this.addKeys(['g'], () => { setCursorIndex(0) }) this.addKeys(['G'], () => { setCursorIndex(this.total - 1) }) let timer: NodeJS.Timeout let firstNumber: number const choose = (n: number) => { let disabled = this.isDisabled(n) if (disabled) return this._onDidClose.fire(n) this.dispose() } this.addKeys(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], character => { if (timer) clearTimeout(timer) let n = parseInt(character, 10) if (isNaN(n) || n > this.total) return if (firstNumber == null && n == 0) return if (firstNumber) { let count = firstNumber * 10 + n firstNumber = undefined choose(count - 1) return } if (this.total < 10 || n * 10 > this.total) { choose(n - 1) return } timer = setTimeout(async () => { choose(n - 1) }, 200) firstNumber = n }) if (this.config.shortcuts) { this.addShortcuts(choose) } } private addShortcuts(choose: (idx: number) => void): void { let { items } = this.config let texts: string[] = items.map(o => { return isMenuItem(o) ? o.text : o }) texts.forEach((text, idx) => { if (text.length) { let s = text[0] if (isAlphabet(s.charCodeAt(0)) && !this.keyMappings.has(s)) { this.shortcutIndexes.add(idx) this.addKeys(s, () => { choose(idx) }) } } }) } private isDisabled(idx: number): boolean { let { items } = this.config let item = items[idx] if (isMenuItem(item) && item.disabled) { return true } return false } public async show(preferences: DialogPreferences = {}): Promise { let { nvim, shortcutIndexes } = this let { title, items, borderhighlight, position, content } = this.config let opts: any = {} if (title) opts.title = title if (position === 'center') opts.relative = 'editor' if (preferences.maxHeight) opts.maxHeight = preferences.maxHeight if (preferences.maxWidth) opts.maxWidth = preferences.maxWidth if (preferences.floatHighlight) opts.highlight = preferences.floatHighlight if (borderhighlight) { opts.borderhighlight = borderhighlight } else if (preferences.floatBorderHighlight) { opts.borderhighlight = preferences.floatBorderHighlight } if (preferences.rounded) opts.rounded = 1 if (typeof content === 'string') opts.content = content if (preferences.confirmKey) { this.addKeys(preferences.confirmKey, () => { this.selectCurrent() }) } let highlights: HighlightItem[] = [] let lines = items.map((v, i) => { let text: string = isMenuItem(v) ? v.text : v let pre = toIndexText(i) if (shortcutIndexes.has(i)) { highlights.push({ lnum: i, hlGroup: preferences.shortcutHighlight || 'MoreMsg', colStart: byteLength(pre), colEnd: byteLength(pre) + 1 }) } return pre + text.trim() }) lines.forEach((line, i) => { let item = items[i] if (isMenuItem(item) && item.disabled) { highlights.push({ hlGroup: 'CocDisabled', lnum: i, colStart: 0, colEnd: byteLength(line) }) } }) if (highlights.length) opts.highlights = highlights let [winid, bufnr, contentHeight] = await nvim.call('coc#dialog#create_menu', [lines, opts]) as [number, number, number] nvim.command('redraw', true) if (this._disposed) return this.win = new Popup(nvim, winid, bufnr, lines.length + contentHeight, contentHeight) this.bufnr = bufnr this.contentHeight = contentHeight this.attachEvents() nvim.call('coc#prompt#start_prompt', ['menu'], true) } private selectCurrent(): void { if (this.isDisabled(this.currIndex)) { let item = this.config.items[this.currIndex] as MenuItem if (item.disabled['reason']) { this.nvim.outWriteLine(`Item disabled: ${item.disabled['reason']}`) } return } this._onDidClose.fire(this.currIndex) this.dispose() } public get buffer(): Buffer { return this.bufnr ? this.nvim.createBuffer(this.bufnr) : undefined } public dispose(): void { if (this._disposed) return this._disposed = true disposeAll(this.disposables) this.shortcutIndexes.clear() this.keyMappings.clear() this.nvim.call('coc#prompt#stop_prompt', ['menu'], true) this.win?.close() this.bufnr = undefined this.win = undefined } public async onInputChar(session: string, character: string): Promise { if (session != 'menu' || !this.win) return let fn = this.keyMappings.get(character) if (fn) await Promise.resolve(fn(character)) } private setCursor(index: number): void { this.currIndex = index - this.contentHeight this.win.setCursor(index) } private addKeys(keys: string | string[], fn: (character: string) => void): void { if (Array.isArray(keys)) { for (let key of keys) { this.keyMappings.set(key, fn) } } else { this.keyMappings.set(keys, fn) } } } ================================================ FILE: src/model/mru.ts ================================================ 'use strict' import { distinct } from '../util/array' import { dataHome } from '../util/constants' import { readFileLines, writeFile } from '../util/fs' import { fs, path, promisify } from '../util/node' /** * Mru - manage string items as lines in mru file. */ export default class Mru { private file: string /** * @param {string} name unique name * @param {string} base? optional directory name, default to data root of coc.nvim */ constructor( name: string, base?: string, private maximum = 5000) { this.file = path.join(base || dataHome, name) let dir = path.dirname(this.file) fs.mkdirSync(dir, { recursive: true }) } /** * Load lines from mru file */ public async load(): Promise { try { let lines = await readFileLines(this.file, 0, this.maximum) if (lines.length > this.maximum) { let newLines = lines.slice(0, this.maximum) await writeFile(this.file, newLines.join('\n')) return distinct(newLines) } return distinct(lines) } catch (e) { return [] } } public loadSync(): string[] { try { let content = fs.readFileSync(this.file, 'utf8') content = content.trim() return content.length ? content.trim().split('\n') : [] } catch (e) { return [] } } /** * Add item to mru file. */ public async add(item: string): Promise { let buf: Buffer try { buf = fs.readFileSync(this.file) if (buf[0] === 239 && buf[1] === 187 && buf[2] === 191) { buf = buf.slice(3) } buf = Buffer.concat([Buffer.from(item, 'utf8'), new Uint8Array([10]), buf]) } catch (e) { buf = Buffer.concat([Buffer.from(item, 'utf8'), new Uint8Array([10])]) } await promisify(fs.writeFile)(this.file, buf) } /** * Remove item from mru file. */ public async remove(item: string): Promise { let items = await this.load() let len = items.length items = items.filter(s => s != item) if (items.length != len) { await writeFile(this.file, items.join('\n')) } } /** * Remove the data file. */ public async clean(): Promise { try { await promisify(fs.unlink)(this.file) } catch (e) { // noop } } } ================================================ FILE: src/model/notification.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import { defaultValue, disposeAll } from '../util' import { toArray } from '../util/array' import { Disposable } from '../util/protocol' import { toText } from '../util/string' import { DialogButton } from './dialog' /** * Represents an action that is shown with an information, warning, or * error message. * @see [showInformationMessage](#window.showInformationMessage) * @see [showWarningMessage](#window.showWarningMessage) * @see [showErrorMessage](#window.showErrorMessage) */ export interface MessageItem { /** * A short title like 'Retry', 'Open Log' etc. */ title: string /** * A hint for modal dialogs that the item should be triggered * when the user cancels the dialog (e.g. by pressing the ESC * key). * * Note: this option is ignored for non-modal messages. * Note: not used by coc.nvim for now. */ isCloseAffordance?: boolean } export interface NotificationPreferences { disabled: boolean maxWidth: number maxHeight: number highlight: string winblend: number border: boolean timeout: number marginRight: number focusable: boolean minWidth?: number source?: string } export type NotificationKind = 'error' | 'info' | 'warning' | 'progress' export interface NotificationConfig { kind?: NotificationKind content?: string /** * Optional title text. */ title?: string /** * Buttons as bottom of dialog. */ buttons?: DialogButton[] /** * index is -1 for window close without button click */ callback?: (index: number) => void closable?: boolean } export function toButtons(texts: string[]): DialogButton[] { return texts.map((s, index) => { return { text: s, index } }) } export function toTitles(items: (string | MessageItem)[]): string[] { return items.map(item => typeof item === 'string' ? item : item.title) } export default class Notification { protected disposables: Disposable[] = [] public bufnr: number protected _winid: number constructor(protected nvim: Neovim, protected config: NotificationConfig, attachEvents = true) { if (attachEvents) { events.on('BufWinLeave', bufnr => { if (bufnr == this.bufnr) { this.dispose() if (config.callback) config.callback(-1) } }, null, this.disposables) let btns = toArray(config.buttons).filter(o => o.disabled != true) events.on('FloatBtnClick', (bufnr, idx) => { if (bufnr == this.bufnr) { this.dispose() if (config.callback) config.callback(defaultValue(btns[idx]?.index, -1)) } }, null, this.disposables) } } protected get lines(): string[] { return this.config.content ? this.config.content.split(/\r?\n/) : [] } public async show(preferences: Partial): Promise { let { nvim } = this let { buttons, kind, title } = this.config let opts: any = Object.assign({}, preferences) opts.kind = toText(kind) opts.close = this.config.closable === true ? 1 : 0 if (title) opts.title = title if (preferences.border) { opts.borderhighlight = kind ? `CocNotification${kind[0].toUpperCase()}${kind.slice(1)}` : preferences.highlight } if (Array.isArray(buttons)) { let actions: string[] = buttons.filter(o => !o.disabled).map(o => o.text) opts.actions = actions } let res = await nvim.call('coc#notify#create', [this.lines, opts]) as [number, number] this._winid = res[0] this.bufnr = res[1] } public get winid(): number | undefined { return this._winid } public dispose(): void { let { winid } = this if (winid) { this.nvim.call('coc#notify#close', [winid], true) this.nvim.redrawVim() } this.bufnr = undefined this._winid = undefined disposeAll(this.disposables) } } ================================================ FILE: src/model/outputChannel.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { OutputChannel } from '../types' function escapeQuote(input: string): string { return input.replace(/'/g, "''") } export default class BufferChannel implements OutputChannel { private lines: string[] = [''] private _disposed = false public created = false constructor(public name: string, private nvim?: Neovim, private onDispose?: () => void) { } public get content(): string { return this.lines.join('\n') } private _append(value: string): void { let { nvim } = this if (!nvim) return let idx = this.lines.length - 1 let newlines = value.split(/\r?\n/) let lastline = this.lines[idx] + newlines[0] this.lines[idx] = lastline let append = newlines.slice(1) this.lines = this.lines.concat(append) if (!this.created) return nvim.pauseNotification() nvim.call('setbufline', [this.bufname, '$', lastline], true) if (append.length) { nvim.call('appendbufline', [this.bufname, '$', append], true) } nvim.resumeNotification(false, true) } public append(value: string): void { if (!this.validate()) return this._append(value) } public appendLine(value: string): void { if (!this.validate()) return this._append(value + '\n') } public clear(keep?: number): void { let { nvim } = this if (!this.validate() || !nvim) return this.lines = keep ? this.lines.slice(-keep) : [] if (!this.created) return nvim.pauseNotification() nvim.call('deletebufline', [this.bufname, 1, '$'], true) if (this.lines.length) { nvim.call('appendbufline', [this.bufname, '$', this.lines], true) } nvim.resumeNotification(true, true) } public hide(): void { this.created = false let name = escapeQuote(this.bufname) if (this.nvim) this.nvim.command(`exe 'silent! bwipeout! '.fnameescape('${name}')`, true) } private get bufname(): string { return `output:///${encodeURI(this.name)}` } public show(preserveFocus?: boolean, cmd = 'vs'): void { let { nvim } = this if (!nvim) return let name = escapeQuote(this.bufname) nvim.pauseNotification() nvim.command(`exe '${cmd} '.fnameescape('${name}')`, true) if (preserveFocus) { nvim.command('wincmd p', true) } nvim.resumeNotification(true, true) this.created = true } private validate(): boolean { return !this._disposed } public dispose(): void { if (this.onDispose) this.onDispose() this._disposed = true this.hide() this.lines = [] } } ================================================ FILE: src/model/picker.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import events from '../events' import { HighlightItem, QuickPickItem } from '../types' import { disposeAll } from '../util' import { CancellationToken, Disposable, Emitter, Event } from '../util/protocol' import { byteLength } from '../util/string' import { DialogPreferences } from './dialog' import Popup from './popup' interface PickerConfig { title: string items: QuickPickItem[] } export function toPickerItems(items: (QuickPickItem | string)[]): QuickPickItem[] { return items.map(item => typeof item === 'string' ? { label: item } : item) } /** * Pick multiple items from dialog */ export default class Picker { public bufnr: number private win: Popup | undefined private picked: Set = new Set() private total: number private disposables: Disposable[] = [] private keyMappings: Map void> = new Map() private readonly _onDidClose = new Emitter() public readonly onDidClose: Event = this._onDidClose.event constructor(private nvim: Neovim, private config: PickerConfig, token?: CancellationToken) { for (let i = 0; i < config.items.length; i++) { let item = config.items[i] if (item.picked) this.picked.add(i) } this.total = config.items.length if (token) { token.onCancellationRequested(() => { this.win?.close() }) } this.disposables.push(this._onDidClose) } public get currIndex(): number { return this.win ? this.win.currIndex : 0 } private attachEvents(): void { events.on('InputChar', this.onInputChar.bind(this), null, this.disposables) events.on('BufWinLeave', bufnr => { if (bufnr == this.bufnr) { this._onDidClose.fire(undefined) this.bufnr = undefined this.win = undefined this.dispose() } }, null, this.disposables) events.on('FloatBtnClick', (bufnr, idx) => { if (bufnr != this.bufnr) return if (idx == 0) { let selected = Array.from(this.picked) this._onDidClose.fire(selected.length > 0 ? selected : undefined) } else { this._onDidClose.fire(undefined) } this.dispose() }, null, this.disposables) this.addKeymappings() } private addKeymappings(): void { let { nvim } = this const toggleSelect = idx => { if (this.picked.has(idx)) { this.picked.delete(idx) } else { this.picked.add(idx) } } this.addKeys('', async () => { // not work on vim // if (isVim) return let [winid, lnum, col] = await nvim.call('coc#ui#get_mouse') as [number, number, number] nvim.pauseNotification() if (winid == this.win.winid) { if (col <= 3) { toggleSelect(lnum - 1) this.changeLine(lnum - 1) } else { this.win.setCursor(lnum - 1) } } nvim.call('win_gotoid', [winid], true) nvim.call('cursor', [lnum, col], true) nvim.call('coc#float#nvim_float_click', [], true) nvim.command('redraw', true) await nvim.resumeNotification() }) this.addKeys(['', ''], () => { this._onDidClose.fire(undefined) this.dispose() }) this.addKeys('', () => { if (this.picked.size == 0) { this._onDidClose.fire(undefined) } else { let selected = Array.from(this.picked) this._onDidClose.fire(selected) } this.dispose() }) this.addKeys(['j', '', '', ''], () => { this.win.setCursor(this.currIndex + 1, true) }) this.addKeys(['k', '', '', ''], () => { this.win.setCursor(this.currIndex - 1, true) }) this.addKeys(['g'], () => { this.win.setCursor(0, true) }) this.addKeys(['G'], () => { this.win.setCursor(this.total - 1, true) }) this.addKeys(' ', async () => { let idx = this.currIndex toggleSelect(idx) nvim.pauseNotification() this.changeLine(idx) this.win.setCursor(this.currIndex + 1) nvim.command('redraw', true) await nvim.resumeNotification() }) this.addKeys('', async () => { await this.win.scrollForward() }) this.addKeys('', async () => { await this.win.scrollBackward() }) } public async show(preferences: DialogPreferences = {}): Promise { let { nvim } = this let { title, items } = this.config let opts: any = { close: 1, cursorline: 1 } if (preferences.maxHeight) opts.maxHeight = preferences.maxHeight if (preferences.maxWidth) opts.maxWidth = preferences.maxWidth if (title) opts.title = title if (preferences.floatHighlight) opts.highlight = preferences.floatHighlight if (preferences.floatBorderHighlight) opts.borderhighlight = [preferences.floatBorderHighlight] if (preferences.pickerButtons) { let shortcut = preferences.pickerButtonShortcut opts.buttons = ['Submit' + (shortcut ? ' ' : ''), 'Cancel' + (shortcut ? ' ' : '')] } if (preferences.rounded) opts.rounded = 1 if (preferences.confirmKey && preferences.confirmKey != '') { this.addKeys(preferences.confirmKey, () => { this._onDidClose.fire(undefined) this.dispose() }) } let lines = [] let highlights: HighlightItem[] = [] for (let i = 0; i < items.length; i++) { let item = items[i] let line = `[${item.picked ? 'x' : ' '}] ${item.label}` if (item.description) { let start = byteLength(line) line = line + ` ${item.description}` highlights.push({ hlGroup: 'Comment', lnum: i, colStart: start, colEnd: byteLength(line) }) } lines.push(line) } if (highlights.length) opts.highlights = highlights let res = await nvim.call('coc#dialog#create_dialog', [lines, opts]) as [number, number] this.win = new Popup(nvim, res[0], res[1], lines.length) this.bufnr = res[1] nvim.call('coc#prompt#start_prompt', ['picker'], true) this.attachEvents() this.win.setCursor(0, true) return res[0] } public get buffer(): Buffer { return this.bufnr ? this.nvim.createBuffer(this.bufnr) : undefined } public dispose(): void { this.picked.clear() this.keyMappings.clear() disposeAll(this.disposables) this.nvim.call('coc#prompt#stop_prompt', ['picker'], true) this.win?.close() this.win = undefined } public async onInputChar(session: string, character: string): Promise { if (session != 'picker' || !this.win) return let fn = this.keyMappings.get(character) if (fn) await Promise.resolve(fn(character)) } public changeLine(index: number): void { let { nvim } = this let item = this.config.items[index] if (!item) return let line = `[${this.picked.has(index) ? 'x' : ' '}] ${item.label}` let col = byteLength(line) if (item.description) line = line + ` ${item.description}` nvim.call('setbufline', [this.bufnr, index + 1, line], true) let buf = nvim.createBuffer(this.bufnr) // eslint-disable-next-line @typescript-eslint/no-floating-promises buf.addHighlight({ hlGroup: 'Comment', line: index, srcId: 1, colStart: col, colEnd: -1 }) } private addKeys(keys: string | string[], fn: (character: string) => void): void { if (Array.isArray(keys)) { for (let key of keys) { this.keyMappings.set(key, fn) } } else { this.keyMappings.set(keys, fn) } } } ================================================ FILE: src/model/popup.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { isVim } from '../util/constants' interface WindowInfo { topline: number, botline: number } /** * More methods for float window/popup */ export default class Popup { constructor( private nvim: Neovim, public readonly winid, public readonly bufnr, public linecount: number, private _currIndex = 0 ) { } public get currIndex(): number { return this._currIndex } public close(): void { this.nvim.call('coc#float#close', [this.winid], true) } public refreshScrollbar(): void { if (!isVim) this.nvim.call('coc#float#nvim_scrollbar', [this.winid], true) } public execute(cmd: string): void { this.nvim.call('win_execute', [this.winid, cmd], true) } private async getWininfo(): Promise { return await this.nvim.call('coc#float#get_wininfo', [this.winid]) as WindowInfo } /** * Simple scroll method, not consider wrapped lines. */ public async scrollForward(): Promise { let { nvim, bufnr } = this let buf = nvim.createBuffer(bufnr) let total = await buf.length let { botline } = await this.getWininfo() if (botline >= total || botline == 0) return nvim.pauseNotification() this.setCursor(botline - 1) this.execute(`silent! noa setl scrolloff=0`) this.execute(`normal! ${botline}Gzt`) this.refreshScrollbar() nvim.command('redraw', true) nvim.resumeNotification(false, true) } /** * Simple scroll method, not consider wrapped lines. */ public async scrollBackward(): Promise { let { nvim } = this let { topline } = await this.getWininfo() if (topline == 1) return nvim.pauseNotification() this.setCursor(topline - 1) this.execute(`normal! ${topline}Gzb`) this.refreshScrollbar() nvim.command('redraw', true) nvim.resumeNotification(false, true) } /** * Move cursor and highlight. */ public setCursor(index: number, redraw = false): void { let { nvim, bufnr, winid, linecount } = this if (index < 0) { index = 0 } else if (index > linecount - 1) { index = linecount - 1 } this._currIndex = index nvim.call('coc#dialog#set_cursor', [winid, bufnr, index + 1], true) if (redraw) { this.refreshScrollbar() nvim.command('redraw', true) } } } ================================================ FILE: src/model/progress.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import { createLogger } from '../logger' import { CancellationToken, CancellationTokenSource, Emitter, Event } from '../util/protocol' import Notification, { NotificationPreferences } from './notification' const logger = createLogger('model-progress') export interface Progress { report(value: { message?: string; increment?: number }): void } export interface ProgressOptions { title?: string cancellable?: boolean task: (progress: Progress, token: CancellationToken) => Thenable } export function formatMessage(title: string | undefined, message: string | undefined, total: number) { let parts = [] if (title) parts.push(title) if (message) parts.push(message) if (total) parts.push(total + '%') return parts.join(' ') } export default class ProgressNotification extends Notification { private tokenSource: CancellationTokenSource private readonly _onDidFinish = new Emitter() public readonly onDidFinish: Event = this._onDidFinish.event constructor(nvim: Neovim, private option: ProgressOptions) { super(nvim, { kind: 'progress', title: option.title, closable: option.cancellable }, false) this.disposables.push(this._onDidFinish) events.on('BufWinLeave', this.cancelProgress, null, this.disposables) } private cancelProgress = (bufnr: any) => { if (bufnr == this.bufnr && this.tokenSource) { this.tokenSource.cancel() } } public async show(preferences: Partial): Promise { let { task } = this.option let tokenSource = this.tokenSource = new CancellationTokenSource() this.disposables.push(tokenSource) let total = 0 if (!preferences.disabled) { await super.show(preferences) } else { logger.warn(`progress window disabled by configuration "notification.disabledProgressSources"`) } task({ report: p => { if (!this.winid) return let { nvim } = this if (p.increment) { total += p.increment nvim.call('coc#window#set_var', [this.winid, 'percent', `${total}%`], true) } if (p.message) nvim.call('coc#window#set_var', [this.winid, 'message', p.message], true) } }, tokenSource.token).then(res => { this._onDidFinish.fire(res) this.dispose() }, err => { if (err) this.nvim.echoError(err) this._onDidFinish.fire(undefined) this.dispose() }) } } ================================================ FILE: src/model/quickpick.ts ================================================ 'use strict' import { Buffer, Neovim } from '@chemzqm/neovim' import events from '../events' import { createLogger } from '../logger' import { HighlightItem, QuickPickItem } from '../types' import { defaultValue, disposeAll } from '../util' import { toArray } from '../util/array' import { FuzzyScorer, anyScore, fuzzyScoreGracefulAggressive } from '../util/filter' import { Disposable, Emitter, Event } from '../util/protocol' import { byteLength, toText } from '../util/string' import { DialogPreferences } from './dialog' import { toSpans } from './fuzzyMatch' import InputBox from './input' import Popup from './popup' import { StrWidth } from './strwidth' const logger = createLogger('quickpick') interface FilteredLine { line: string score: number index: number spans: [number, number][] descriptionSpan?: [number, number] } /** * Pick single/multiple items from prompt list. */ export default class QuickPick { public title: string public loading: boolean public items: readonly T[] public activeItems: readonly T[] public selectedItems: T[] public value: string public canSelectMany = false public matchOnDescription = false public maxHeight = 30 public width: number | undefined public placeholder: string | undefined private bufnr: number private win: Popup private filteredItems: readonly T[] = [] private disposables: Disposable[] = [] private input: InputBox | undefined private _changed = false // emitted with selected items or undefined when cancelled. private readonly _onDidFinish = new Emitter() private readonly _onDidChangeSelection = new Emitter>() private readonly _onDidChangeValue = new Emitter() public readonly onDidFinish: Event = this._onDidFinish.event public readonly onDidChangeSelection: Event> = this._onDidChangeSelection.event public readonly onDidChangeValue: Event = this._onDidChangeValue.event constructor(private nvim: Neovim, private preferences: DialogPreferences = {}) { let items = [] let input = this.input = new InputBox(this.nvim, '') if (preferences.maxHeight) this.maxHeight = preferences.maxHeight Object.defineProperty(this, 'items', { set: (list: T[]) => { items = toArray(list) this.selectedItems = items.filter(o => o.picked) this.filterItems('') }, get: () => items }) Object.defineProperty(this, 'activeItems', { set: (list: T[]) => { items = toArray(list) this.filteredItems = items this.showFilteredItems() }, get: () => this.filteredItems }) Object.defineProperty(this, 'value', { set: (value: string) => { this.input.value = value }, get: () => this.input.value }) Object.defineProperty(this, 'title', { set: (newTitle: string) => { input.title = toText(newTitle) }, get: () => input.title ?? '' }) Object.defineProperty(this, 'loading', { set: (loading: boolean) => { input.loading = loading }, get: () => input.loading }) input.onDidChange(value => { this._changed = false this._onDidChangeValue.fire(value) // List already update by change items or activeItems if (this._changed) { this._changed = false return } this.filterItems(value) }, this) input.onDidFinish(this.onFinish, this) } public get maxWidth(): number { return this.preferences.maxWidth ?? 80 } public get currIndex(): number { return this.win ? this.win.currIndex : 0 } public get buffer(): Buffer { return this.bufnr ? this.nvim.createBuffer(this.bufnr) : undefined } public get winid(): number | undefined { return this.win?.winid } public get inputBox(): InputBox | undefined { return this.input } public setCursor(index: number): void { this.win?.setCursor(index, true) } private attachEvents(inputBufnr: number): void { events.on('BufWinLeave', bufnr => { if (bufnr == this.bufnr) { this.bufnr = undefined this.win = undefined } }, null, this.disposables) events.on('InputListSelect', index => { if (index >= 0) { this.setCursor(index) } this.onFinish(index < 0 ? undefined : '') }, null, this.disposables) events.on('PromptKeyPress', async (bufnr, key) => { if (bufnr == inputBufnr) { if (key == '') { await this.win.scrollForward() } else if (key == '') { await this.win.scrollBackward() } else if (['', '', ''].includes(key)) { this.setCursor(this.currIndex + 1) } else if (['', '', ''].includes(key)) { this.setCursor(this.currIndex - 1) } else if (this.canSelectMany && ['', ''].includes(key)) { this.toggePicked(this.currIndex) } } }, null, this.disposables) } public async show(): Promise { let { nvim, items, input, width, preferences, maxHeight } = this let { lines, highlights } = this.buildList(items, input.value) let minWidth: number | undefined let lincount = 0 const sw = await StrWidth.create() if (typeof width === 'number') minWidth = Math.min(width, this.maxWidth) let max = 40 lines.forEach(line => { let w = sw.getWidth(line) if (typeof minWidth === 'number') { lincount += Math.ceil(w / minWidth) } else { if (w >= 80) { minWidth = 80 lincount += Math.ceil(w / minWidth) } else { max = Math.max(max, w) lincount += 1 } } }) if (minWidth === undefined) minWidth = max let rounded = !!preferences.rounded await input.show(this.title, { quickpick: lines, position: 'center', placeHolder: this.placeholder, marginTop: 10, border: [1, 1, 0, 1], list: true, rounded, minWidth, maxWidth: this.maxWidth, highlight: preferences.floatHighlight, borderhighlight: preferences.floatBorderHighlight }) let opts: any = { lines, rounded, maxHeight, highlights, linecount: Math.max(1, lincount) } opts.highlight = defaultValue(preferences.floatHighlight, undefined) opts.borderhighlight = defaultValue(preferences.floatBorderHighlight, undefined) let res = await nvim.call('coc#dialog#create_list', [input.winid, input.dimension, opts]) if (!res) throw new Error('Unable to open list window.') // let height this.win = new Popup(nvim, res[0], res[1], lines.length) this.win.refreshScrollbar() this.bufnr = res[1] this.setCursor(0) this.attachEvents(input.bufnr) } private buildList(items: ReadonlyArray, input: string, loose = false): { lines: string[], highlights: HighlightItem[] } { let { selectedItems, canSelectMany } = this let filteredItems: T[] = [] let filtered: FilteredLine[] = [] let emptyInput = input.length === 0 let lowInput = input.toLowerCase() const scoreFn: FuzzyScorer = loose ? anyScore : fuzzyScoreGracefulAggressive const wordPos = canSelectMany ? 4 : 0 for (let index = 0; index < items.length; index++) { const item = items[index] let filterText = this.toFilterText(item) let spans: [number, number][] = [] let score = 0 let descriptionSpan: [number, number] | undefined if (!emptyInput) { let res = scoreFn(input, lowInput, 0, filterText, filterText.toLowerCase(), wordPos, { boostFullMatch: false, firstMatchCanBeWeak: true }) if (!res) continue // keep the order for loose match score = loose ? 0 : res[0] spans = toSpans(filterText, res) } let picked = selectedItems.includes(item) let line = canSelectMany ? `[${picked ? 'x' : ' '}] ${item.label}` : item.label if (item.description) { let start = byteLength(line) line = line + ` ${item.description}` descriptionSpan = [start, start + 1 + byteLength(item.description)] } let lineItem: FilteredLine = { line, descriptionSpan, index, score, spans } filtered.push(lineItem) } let lines: string[] = [] let highlights: HighlightItem[] = [] filtered.sort((a, b) => { if (a.score != b.score) return b.score - a.score return a.index - b.index }) const toHighlight = (lnum: number, span: [number, number], hlGroup: string, pre: number) => { return { lnum, colStart: span[0] + pre, colEnd: span[1] + pre, hlGroup } } filtered.forEach((item, index) => { lines.push(item.line) item.spans.forEach(span => { highlights.push(toHighlight(index, span, 'CocSearch', wordPos)) }) if (item.descriptionSpan) { highlights.push(toHighlight(index, item.descriptionSpan, 'Comment', 0)) } filteredItems.push(items[item.index]) }) this.filteredItems = filteredItems return { lines, highlights } } /** * Filter items, does highlight only when loose is true */ private _filter(items: ReadonlyArray, input: string, loose = false): void { if (!this.win) return this._changed = true let { lines, highlights } = this.buildList(items, input, loose) this.nvim.call('coc#dialog#update_list', [this.win.winid, this.win.bufnr, lines, highlights], true) this.win.linecount = lines.length this.setCursor(0) } /** * Filter items with input */ public filterItems(input: string): void { this._filter(this.items, input) } public showFilteredItems(): void { let { input, filteredItems } = this this._filter(filteredItems, input.value, true) } private onFinish(input: string | undefined): void { let items = input == null ? null : this.getSelectedItems() if (!this.canSelectMany && input !== undefined && Array.isArray(items)) { this._onDidChangeSelection.fire(items) } this.nvim.call('coc#float#close', [this.winid], true) // needed to make sure window closed setTimeout(() => { this._onDidFinish.fire(items) this.dispose() }, 30) } private getSelectedItems(): T[] { let { canSelectMany } = this if (canSelectMany) return this.selectedItems return toArray(this.filteredItems[this.currIndex]) } public toggePicked(index: number): void { let { nvim, filteredItems, selectedItems } = this let item = filteredItems[index] if (!item) return let idx = selectedItems.indexOf(item) if (idx != -1) { selectedItems.splice(idx, 1) } else { selectedItems.push(item) } let text = idx == -1 ? 'x' : ' ' nvim.pauseNotification() this.win.execute(`normal! ^1lr${text}`) this.win.setCursor(this.win.currIndex + 1) nvim.resumeNotification(true, true) this._onDidChangeSelection.fire(selectedItems) } private toFilterText(item: T): string { let { label, description } = item let { canSelectMany } = this let line = `${canSelectMany ? ' ' : ''}${label.replace(/\r?\n/, '')}` return this.matchOnDescription ? line + ' ' + (description ?? '') : line } public dispose(): void { this.bufnr = undefined this.input.dispose() this.win?.close() this._onDidFinish.dispose() this._onDidChangeSelection.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/model/regions.ts ================================================ 'use strict' /** * Remember used regions */ export default class Regions { /** * ranges that never overlaps. */ private ranges: [number, number][] = [] public get current(): ReadonlyArray { let res: number[] = [] this.ranges.sort((a, b) => a[0] - b[0]) this.ranges.forEach(o => { res.push(o[0], o[1]) }) return res } public get isEmpty(): boolean { return this.ranges.length === 0 } public clear(): void { this.ranges = [] } public getRange(line: number): [number, number] | undefined { for (const [start, end] of this.ranges) { if (line >= start && line <= end) return [start, end] } return undefined } /** * Get the span that not covered yet, all 0 based */ public toUncoveredSpan(span: [number, number], delta: number, max: number): [number, number] | undefined { let [start, end] = span start = Math.max(0, start - delta) end = Math.min(max, end + delta) let r = this.getRange(start) start = r ? r[1] : start let s = this.getRange(end) // start, end in the same range if (s && r && s[0] === r[0] && s[1] === r[1]) return undefined end = s ? s[0] : end return [start, end] } /** * start, end 0 based, both inclusive */ public add(start: number, end: number): void { if (start > end) { [start, end] = [end, start] } let { ranges } = this if (ranges.length == 0) { ranges.push([start, end]) } else { // 1, 2, 3 ranges.sort((a, b) => a[0] - b[0]) let s: number let e: number let removedIndexes: number[] = [] for (let i = 0; i < ranges.length; i++) { let r = ranges[i] if (r[1] < start - 1) continue if (r[0] > end + 1) break removedIndexes.push(i) if (s == null) s = Math.min(start, r[0]) e = Math.max(end, r[1]) } let newRanges = removedIndexes.length ? ranges.filter((_, i) => !removedIndexes.includes(i)) : ranges this.ranges = newRanges if (s != null && e != null) { this.ranges.push([s, e]) } else { this.ranges.push([start, end]) } } } public has(start: number, end: number): boolean { let idx = this.ranges.findIndex(o => o[0] <= start && o[1] >= end) return idx !== -1 } public static mergeSpans(ranges: [number, number][]): [number, number][] { let res: [number, number][] = [] for (let r of ranges) { let idx = res.findIndex(o => !(r[1] < o[0] || r[0] > o[1])) if (idx == -1) { res.push(r) } else { let o = res[idx] res[idx] = [Math.min(r[0], o[0]), Math.max(r[1], o[1])] } } return res } } ================================================ FILE: src/model/relativePattern.ts ================================================ 'use strict' import { URI } from 'vscode-uri' import { illegalArgument } from '../util/errors' import { WorkspaceFolder } from 'vscode-languageserver-types' export default class RelativePattern { public pattern: string public baseUri: URI constructor(base: WorkspaceFolder | URI | string, pattern: string) { if (typeof base !== 'string') { if (!base || !URI.isUri(base) && typeof base.uri !== 'string') { throw illegalArgument('base') } } if (typeof pattern !== 'string') { throw illegalArgument('pattern') } if (typeof base === 'string') { this.baseUri = URI.file(base) } else if (URI.isUri(base)) { this.baseUri = base } else { this.baseUri = URI.parse(base.uri) } this.pattern = pattern } public toJSON() { return { pattern: this.pattern, baseUri: this.baseUri.toJSON() } } } ================================================ FILE: src/model/resolver.ts ================================================ 'use strict' import { fs, path } from '../util/node' import { statAsync } from '../util/fs' import { executable, runCommand } from '../util/processes' import stripAnsi from 'strip-ansi' export default class Resolver { private _npmFolder: string private _yarnFolder: string public get nodeFolder(): Promise { if (!executable('npm')) return Promise.resolve('') if (this._npmFolder) return Promise.resolve(this._npmFolder) return runCommand('npm --loglevel silent root -g', {}, 3000).then(root => { this._npmFolder = stripAnsi(root).trim() return this._npmFolder }) } public get yarnFolder(): Promise { if (!executable('yarnpkg')) return Promise.resolve('') if (this._yarnFolder) return Promise.resolve(this._yarnFolder) return runCommand('yarnpkg global dir', {}, 3000).then(root => { let folder = path.join(stripAnsi(root).trim(), 'node_modules') let exists = fs.existsSync(folder) if (exists) this._yarnFolder = folder return exists ? folder : '' }) } public async resolveModule(mod: string): Promise { let nodeFolder = await this.nodeFolder let yarnFolder = await this.yarnFolder if (yarnFolder) { let s = await statAsync(path.join(yarnFolder, mod, 'package.json')) if (s && s.isFile()) return path.join(yarnFolder, mod) } if (nodeFolder) { let s = await statAsync(path.join(nodeFolder, mod, 'package.json')) if (s && s.isFile()) return path.join(nodeFolder, mod) } return null } } ================================================ FILE: src/model/semanticTokensBuilder.ts ================================================ 'use strict' import { Range, SemanticTokens, SemanticTokensLegend } from "vscode-languageserver-types" function isStringArray(value: any): value is string[] { return Array.isArray(value) && (value as any[]).every(elem => typeof elem === 'string') } function isStrArrayOrUndefined(arg: any): arg is string[] | undefined { return ((typeof arg === 'undefined') || isStringArray(arg)) } /** * A semantic tokens builder can help with creating a `SemanticTokens` instance * which contains delta encoded semantic tokens. */ export class SemanticTokensBuilder { private _prevLine: number private _prevChar: number private _dataIsSortedAndDeltaEncoded: boolean private _data: number[] private _dataLen: number private _tokenTypeStrToInt: Map private _tokenModifierStrToInt: Map private _hasLegend: boolean constructor(legend?: SemanticTokensLegend) { this._prevLine = 0 this._prevChar = 0 this._dataIsSortedAndDeltaEncoded = true this._data = [] this._dataLen = 0 this._tokenTypeStrToInt = new Map() this._tokenModifierStrToInt = new Map() this._hasLegend = false if (legend) { this._hasLegend = true for (let i = 0, len = legend.tokenTypes.length; i < len; i++) { this._tokenTypeStrToInt.set(legend.tokenTypes[i], i) } for (let i = 0, len = legend.tokenModifiers.length; i < len; i++) { this._tokenModifierStrToInt.set(legend.tokenModifiers[i], i) } } } /** * Add another token. * @param line The token start line number (absolute value). * @param char The token start character (absolute value). * @param length The token length in characters. * @param tokenType The encoded token type. * @param tokenModifiers The encoded token modifiers. */ public push(line: number, char: number, length: number, tokenType: number, tokenModifiers?: number): void /** * Add another token. Use only when providing a legend. * @param range The range of the token. Must be single-line. * @param tokenType The token type. * @param tokenModifiers The token modifiers. */ public push(range: Range, tokenType: string, tokenModifiers?: string[]): void public push(arg0: any, arg1: any, arg2: any, arg3?: any, arg4?: any): void { if (typeof arg0 === 'number' && typeof arg1 === 'number' && typeof arg2 === 'number' && typeof arg3 === 'number' && (typeof arg4 === 'number' || typeof arg4 === 'undefined')) { if (typeof arg4 === 'undefined') { arg4 = 0 } // 1st overload return this._pushEncoded(arg0, arg1, arg2, arg3, arg4) } if (Range.is(arg0) && typeof arg1 === 'string' && isStrArrayOrUndefined(arg2)) { // 2nd overload return this._push(arg0, arg1, arg2) } throw new Error('Illegal argument') } private _push(range: Range, tokenType: string, tokenModifiers?: string[]): void { if (!this._hasLegend) { throw new Error('Legend must be provided in constructor') } if (range.start.line !== range.end.line) { throw new Error('`range` cannot span multiple lines') } if (!this._tokenTypeStrToInt.has(tokenType)) { throw new Error('`tokenType` is not in the provided legend') } const line = range.start.line const char = range.start.character const length = range.end.character - range.start.character const nTokenType = this._tokenTypeStrToInt.get(tokenType)! let nTokenModifiers = 0 if (tokenModifiers) { for (const tokenModifier of tokenModifiers) { if (!this._tokenModifierStrToInt.has(tokenModifier)) { throw new Error('`tokenModifier` is not in the provided legend') } const nTokenModifier = this._tokenModifierStrToInt.get(tokenModifier)! nTokenModifiers |= (1 << nTokenModifier) >>> 0 } } this._pushEncoded(line, char, length, nTokenType, nTokenModifiers) } private _pushEncoded(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void { if (this._dataIsSortedAndDeltaEncoded && (line < this._prevLine || (line === this._prevLine && char < this._prevChar))) { // push calls were ordered and are no longer ordered this._dataIsSortedAndDeltaEncoded = false // Remove delta encoding from data const tokenCount = (this._data.length / 5) | 0 let prevLine = 0 let prevChar = 0 for (let i = 0; i < tokenCount; i++) { let line = this._data[5 * i] let char = this._data[5 * i + 1] if (line === 0) { // on the same line as previous token line = prevLine char += prevChar } else { // on a different line than previous token line += prevLine } this._data[5 * i] = line this._data[5 * i + 1] = char prevLine = line prevChar = char } } let pushLine = line let pushChar = char if (this._dataIsSortedAndDeltaEncoded && this._dataLen > 0) { pushLine -= this._prevLine if (pushLine === 0) { pushChar -= this._prevChar } } this._data[this._dataLen++] = pushLine this._data[this._dataLen++] = pushChar this._data[this._dataLen++] = length this._data[this._dataLen++] = tokenType this._data[this._dataLen++] = tokenModifiers this._prevLine = line this._prevChar = char } private static _sortAndDeltaEncode(data: number[]): number[] { let pos: number[] = [] const tokenCount = (data.length / 5) | 0 for (let i = 0; i < tokenCount; i++) { pos[i] = i } pos.sort((a, b) => { const aLine = data[5 * a] const bLine = data[5 * b] if (aLine === bLine) { const aChar = data[5 * a + 1] const bChar = data[5 * b + 1] return aChar - bChar } return aLine - bLine }) const result = new Array(data.length) let prevLine = 0 let prevChar = 0 for (let i = 0; i < tokenCount; i++) { const srcOffset = 5 * pos[i] const line = data[srcOffset + 0] const char = data[srcOffset + 1] const length = data[srcOffset + 2] const tokenType = data[srcOffset + 3] const tokenModifiers = data[srcOffset + 4] const pushLine = line - prevLine const pushChar = (pushLine === 0 ? char - prevChar : char) const dstOffset = 5 * i result[dstOffset + 0] = pushLine result[dstOffset + 1] = pushChar result[dstOffset + 2] = length result[dstOffset + 3] = tokenType result[dstOffset + 4] = tokenModifiers prevLine = line prevChar = char } return result } /** * Finish and create a `SemanticTokens` instance. */ public build(resultId?: string): SemanticTokens { if (!this._dataIsSortedAndDeltaEncoded) { return { data: SemanticTokensBuilder._sortAndDeltaEncode(this._data), resultId } } return { data: this._data, resultId } } } ================================================ FILE: src/model/status.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { v1 as uuidv1 } from 'uuid' import type { Disposable } from '../util/protocol' export const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] export interface StatusBarItem { /** * The priority of this item. Higher value means the item should * be shown more to the left. */ readonly priority: number isProgress: boolean text: string show(): void hide(): void dispose(): void } export default class StatusLine implements Disposable { private items: Map = new Map() private shownIds: Set = new Set() private _text = '' private interval: NodeJS.Timeout public nvim: Neovim constructor() { this.interval = setInterval(() => { this.setStatusText() }, 100).unref() } public dispose(): void { this.items.clear() this.shownIds.clear() clearInterval(this.interval) } public reset(): void { this.items.clear() this.shownIds.clear() } public createStatusBarItem(priority: number, isProgress = false): StatusBarItem { let uid = uuidv1() let item: StatusBarItem = { text: '', priority, isProgress, show: () => { this.shownIds.add(uid) this.setStatusText() }, hide: () => { this.shownIds.delete(uid) this.setStatusText() }, dispose: () => { this.shownIds.delete(uid) this.items.delete(uid) this.setStatusText() } } this.items.set(uid, item) return item } private getText(): string { if (this.shownIds.size == 0) return '' let d = new Date() let idx = Math.floor(d.getMilliseconds() / 100) let text = '' let items: StatusBarItem[] = [] for (let [id, item] of this.items) { if (this.shownIds.has(id)) { items.push(item) } } items.sort((a, b) => a.priority - b.priority) for (let item of items) { if (!item.isProgress) { text = `${text} ${item.text}` } else { text = `${text} ${frames[idx]} ${item.text}` } } return text } private setStatusText(): void { let text = this.getText() let { nvim } = this if (text != this._text && nvim) { this._text = text nvim.pauseNotification() this.nvim.setVar('coc_status', text, true) this.nvim.callTimer('coc#util#do_autocmd', ['CocStatusChange'], true) nvim.resumeNotification(false, true) } } } ================================================ FILE: src/model/strwidth.ts ================================================ import { pluginRoot } from '../util/constants' import { fs, path, promisify } from '../util/node' export interface StrWidthWasi { strWidth: (textPtr: number) => number setAmbw: (ambiguousAsDouble: number) => void malloc: (size: number) => number free: (ptr: number) => void memory: { buffer: ArrayBuffer } } const wasmPath = path.join(pluginRoot, 'bin/strwidth.wasm') export async function initStrWidthWasm(): Promise { const buffer = await promisify(fs.readFile)(wasmPath) const res = await global.WebAssembly.instantiate(buffer, { env: {} }) return res.instance.exports as StrWidthWasi } let instance: StrWidth export class StrWidth { private contentPtr: number | undefined private bytes: Uint8Array private cache: Map = new Map() constructor(private exports: StrWidthWasi) { this.bytes = new Uint8Array(exports.memory.buffer) this.contentPtr = exports.malloc(4096) } public setAmbw(ambiguousAsDouble: boolean): void { this.exports.setAmbw(ambiguousAsDouble ? 1 : 0) this.cache.clear() } public getWidth(content: string, cache = false): number { let l = content.length if (l === 0) return 0 if (l > 4095) { content = content.slice(0, 4095) } if (cache && this.cache.has(content)) { return this.cache.get(content) } let { contentPtr } = this let buf = Buffer.from(content, 'utf8') let len = buf.length this.bytes.set(buf, contentPtr) this.bytes[contentPtr + len] = 0 let res = this.exports.strWidth(contentPtr) if (cache) this.cache.set(content, res) return res } public static async create(): Promise { if (instance) return instance let api = await initStrWidthWasm() instance = new StrWidth(api) return instance } } ================================================ FILE: src/model/tabs.ts ================================================ import { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import type Editors from '../core/editors' import { Emitter, Event } from '../util/protocol' export interface TabsModel { onClose: Event> onOpen: Event> isActive(document: TextDocument | URI): boolean isVisible(document: TextDocument | URI): boolean getTabResources(): Set } /** * Track visible documents */ export default class Tabs implements TabsModel { private open: Set = new Set() private readonly _onOpen: Emitter> private readonly _onClose: Emitter> constructor( private editors: Editors ) { this._onOpen = new Emitter() this._onClose = new Emitter() this.editors.onDidChangeVisibleTextEditors(editors => { let uris = Array.from(this.open) let seen: Set = new Set() let opened: Set = new Set() let closed: Set = new Set() for (let editor of editors) { if (!seen.has(editor.uri) && !uris.includes(editor.uri)) { this.open.add(editor.uri) opened.add(URI.parse(editor.uri)) } seen.add(editor.uri) } for (let uri of uris) { if (!seen.has(uri)) { this.open.delete(uri) closed.add(URI.parse(uri)) } } if (opened.size > 0) { this._onOpen.fire(opened) } if (closed.size > 0) { this._onClose.fire(closed) } }) } public attach(): void { for (let editor of this.editors.visibleTextEditors) { this.open.add(editor.uri) } } public get onClose(): Event> { return this._onClose.event } public get onOpen(): Event> { return this._onOpen.event } public isActive(document: TextDocument | URI): boolean { const uri = document instanceof URI ? document : document.uri return this.editors.activeTextEditor?.document.uri === uri.toString() } public isVisible(document: TextDocument | URI): boolean { const uri = document instanceof URI ? document : document.uri return this.open.has(uri.toString()) } public getTabResources(): Set { const result: Set = new Set() let seen: Set = new Set() for (let editor of this.editors.visibleTextEditors) { if (!seen.has(editor.uri)) { result.add(URI.parse(editor.uri)) seen.add(editor.uri) } } return result } } ================================================ FILE: src/model/task.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import events from '../events' import { disposeAll } from '../util' import { Disposable, Emitter, Event } from '../util/protocol' export interface TaskOptions { cmd: string args?: string[] cwd?: string pty?: boolean env?: { [key: string]: string } detach?: boolean } /** * Controls long running task started by vim. * Useful to keep the task running after CocRestart. * @public */ export default class Task implements Disposable { private disposables: Disposable[] = [] private readonly _onExit = new Emitter() private readonly _onStderr = new Emitter() private readonly _onStdout = new Emitter() public readonly onExit: Event = this._onExit.event public readonly onStdout: Event = this._onStdout.event public readonly onStderr: Event = this._onStderr.event /** * @param {Neovim} nvim * @param {string} id unique id */ constructor(private nvim: Neovim, private id: string) { events.on('TaskExit', (id, code) => { if (id == this.id) { this._onExit.fire(code) } }, null, this.disposables) events.on('TaskStderr', (id, lines) => { if (id == this.id) { this._onStderr.fire(lines) } }, null, this.disposables) events.on('TaskStdout', (id, lines) => { if (id == this.id) { this._onStdout.fire(lines) } }, null, this.disposables) } /** * Start task, task will be restarted when already running. * @param {TaskOptions} opts * @returns {Promise} */ public async start(opts: TaskOptions): Promise { let { nvim } = this return await nvim.call('coc#task#start', [this.id, opts]) as boolean } /** * Stop task by SIGTERM or SIGKILL */ public async stop(): Promise { let { nvim } = this await nvim.call('coc#task#stop', [this.id]) } /** * Check if the task is running. */ public get running(): Promise { let { nvim } = this return nvim.call('coc#task#running', [this.id]) as Promise } /** * Stop task and dispose all events. */ public dispose(): void { let { nvim } = this nvim.call('coc#task#stop', [this.id], true) this._onStdout.dispose() this._onStderr.dispose() this._onExit.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/model/terminal.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' export interface TerminalOptions { /** * A human-readable string which will be used to represent the terminal in the UI. */ name?: string /** * A path to a custom shell executable to be used in the terminal. */ shellPath?: string /** * Args for the custom shell executable, this does not work on Windows (see #8429) */ shellArgs?: string[] /** * A path or URI for the current working directory to be used for the terminal. */ cwd?: string /** * Object with environment variables that will be added to the VS Code process. */ env?: { [key: string]: string | null } /** * Whether the terminal process environment should be exactly as provided in * `TerminalOptions.env`. When this is false (default), the environment will be based on the * window's environment and also apply configured platform settings like * `terminal.integrated.windows.env` on top. When this is true, the complete environment * must be provided as nothing will be inherited from the process or any configuration. */ strictEnv?: boolean } export interface TerminalExitStatus { code: number | undefined } export class TerminalModel { public bufnr: number private pid = 0 public exitStatus: TerminalExitStatus | undefined constructor(private cmd: string, private args: string[], private nvim: Neovim, private _name?: string, private strictEnv?: boolean ) { } public async start(cwd?: string, env?: { [key: string]: string | null }): Promise { let { nvim } = this let cmd = [this.cmd, ...this.args] let [bufnr, pid] = await nvim.call('coc#terminal#start', [cmd, cwd, env || {}, !!this.strictEnv]) as [number, number] this.bufnr = bufnr this.pid = pid } public onExit(code: number | undefined): void { this.exitStatus = { code: code === -1 ? undefined : code } } public get name(): string { return this._name || this.cmd } public get processId(): Promise { return Promise.resolve(this.pid) } public sendText(text: string, addNewLine = true): void { if (!this.bufnr) return this.nvim.call('coc#terminal#send', [this.bufnr, text, addNewLine], true) } public async show(preserveFocus?: boolean): Promise { let { bufnr, nvim } = this if (!bufnr) return false return await nvim.call('coc#terminal#show', [bufnr, { preserveFocus }]) as boolean } public async hide(): Promise { let { bufnr, nvim } = this if (!bufnr) return await nvim.eval(`coc#window#close(bufwinid(${bufnr}))`) } public dispose(): void { if (!this.exitStatus) { this.exitStatus = { code: undefined } } let { bufnr, nvim } = this if (!bufnr) return this.bufnr = undefined nvim.call('coc#terminal#close', [bufnr], true) } } ================================================ FILE: src/model/textdocument.ts ================================================ 'use strict' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, Range } from 'vscode-languageserver-types' import { getRangeText } from '../util/textedit' import { TextLine } from './textline' export function computeLinesOffsets(lines: ReadonlyArray, eol: boolean): number[] { const result: number[] = [] let textOffset = 0 for (let line of lines) { result.push(textOffset) textOffset += line.length + 1 } if (eol) result.push(textOffset) return result } /** * Get the first different line */ export function firstDiffLine(oldLines: ReadonlyArray, newLines: ReadonlyArray): [number, string, string] | undefined { let m = Math.max(oldLines.length, newLines.length) for (let i = 0; i < m; i++) { let oldLine = oldLines[i] let newLine = newLines[i] if (oldLine !== newLine) { return [i + 1, oldLine ?? '', newLine ?? ''] } } return undefined } /** * Text document that created with readonly lines. * * Created for save memory since we could reuse readonly lines. */ export class LinesTextDocument implements TextDocument { private _lineOffsets: number[] | undefined private _content: string constructor( public readonly uri: string, public readonly languageId: string, public readonly version: number, public lines: ReadonlyArray, public readonly bufnr: number, public readonly eol: boolean ) { } private get content(): string { if (!this._content) { this._content = this.lines.join('\n') + (this.eol ? '\n' : '') } return this._content } public get length(): number { if (!this._content) { let n = this.lines.reduce((p, c) => { return p + c.length + 1 }, 0) return this.eol ? n : n - 1 } return this._content.length } public get end(): Position { let len = this.lines.length if (this.eol) return Position.create(len, 0) return Position.create(len - 1, this.lines[len - 1].length) } public get lineCount(): number { return this.lines.length + (this.eol ? 1 : 0) } public intersectWith(range: Range): Range { let start: Position = Position.create(0, 0) if (start.line < range.start.line) { start = range.start } else if (range.start.line === start.line) { start = Position.create(start.line, Math.max(start.character, range.start.character)) } let end: Position = this.end if (range.end.line < end.line) { end = range.end } else if (range.end.line === end.line) { end = Position.create(end.line, Math.min(end.character, range.end.character)) } return Range.create(start, end) } public getText(range?: Range): string { if (range) return getRangeText(this.lines, range) return this.content } public lineAt(lineOrPos: number | Position): TextLine { const line = Position.is(lineOrPos) ? lineOrPos.line : lineOrPos if (typeof line !== 'number' || line < 0 || line >= this.lineCount || Math.floor(line) !== line) { throw new Error('Illegal value for `line`') } return new TextLine(line, this.lines[line] ?? '', line === this.lineCount - 1) } public positionAt(offset: number): Position { offset = Math.max(Math.min(offset, this.content.length), 0) let lineOffsets = this.getLineOffsets() let low = 0 let high = lineOffsets.length if (high === 0) { return { line: 0, character: offset } } while (low < high) { let mid = Math.floor((low + high) / 2) if (lineOffsets[mid] > offset) { high = mid } else { low = mid + 1 } } // low is the least x for which the line offset is larger than the current offset // or array.length if no line offset is larger than the current offset let line = low - 1 return { line, character: offset - lineOffsets[line] } } public offsetAt(position: Position) { let lineOffsets = this.getLineOffsets() if (position.line >= lineOffsets.length) { return this.content.length } else if (position.line < 0) { return 0 } let lineOffset = lineOffsets[position.line] let nextLineOffset = (position.line + 1 < lineOffsets.length) ? lineOffsets[position.line + 1] : this.content.length return Math.max(Math.min(lineOffset + position.character, nextLineOffset), lineOffset) } private getLineOffsets(): number[] { if (this._lineOffsets === undefined) { this._lineOffsets = computeLinesOffsets(this.lines, this.eol) } return this._lineOffsets } } ================================================ FILE: src/model/textline.ts ================================================ 'use strict' import { Range } from 'vscode-languageserver-types' /** * Represents a line of text, such as a line of source code. * * TextLine objects are __immutable__. When a {@link TextDocument document} changes, * previously retrieved lines will not represent the latest state. */ export class TextLine { private readonly _line: number private readonly _text: string private readonly _isLastLine: boolean constructor(line: number, text: string, isLastLine: boolean) { this._line = line this._text = text this._isLastLine = isLastLine } /** * The zero-based line number. */ public get lineNumber(): number { return this._line } /** * The text of this line without the line separator characters. */ public get text(): string { return this._text } /** * The range this line covers without the line separator characters. */ public get range(): Range { return Range.create(this._line, 0, this._line, this._text.length) } /** * The range this line covers with the line separator characters. */ public get rangeIncludingLineBreak(): Range { return this._isLastLine ? this.range : Range.create(this._line, 0, this._line + 1, 0) } /** * The offset of the first character which is not a whitespace character as defined * by `/\s/`. **Note** that if a line is all whitespace the length of the line is returned. */ public get firstNonWhitespaceCharacterIndex(): number { return /^(\s*)/.exec(this._text)![1].length } /** * Whether this line is whitespace only, shorthand * for {@link TextLine.firstNonWhitespaceCharacterIndex} === {@link TextLine.text TextLine.text.length}. */ public get isEmptyOrWhitespace(): boolean { return this.firstNonWhitespaceCharacterIndex === this._text.length } } ================================================ FILE: src/plugin.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { CallHierarchyItem, CodeAction, CodeActionKind, InsertTextMode, Range, WorkspaceSymbol } from 'vscode-languageserver-types' import commandManager from './commands' import completion, { Completion } from './completion' import sources from './completion/sources' import type { CompleteFinishKind } from './completion/types' import Cursors from './cursors' import diagnosticManager from './diagnostic/manager' import events from './events' import extensions from './extension' import Handler from './handler' import { AcceptKind, InlineSuggestOption } from './handler/inline' import listManager from './list/manager' import { createLogger } from './logger' import services from './services' import snippetManager from './snippets/manager' import { HoverTarget, UltiSnippetOption } from './types' import { Disposable, disposeAll, getConditionValue } from './util' import window, { Window } from './window' import workspace, { Workspace } from './workspace' const logger = createLogger('plugin') export type Callback = (...args: any[]) => unknown export default class Plugin { private ready = false private initialized = false public handler: Handler | undefined private cursors: Cursors private actions: Map = new Map() private disposables: Disposable[] = [] constructor(public nvim: Neovim) { Object.defineProperty(window, 'workspace', { get: () => workspace }) Object.defineProperty(workspace, 'nvim', { get: () => this.nvim }) Object.defineProperty(window, 'nvim', { get: () => this.nvim }) Object.defineProperty(window, 'cursors', { get: () => this.cursors }) Object.defineProperty(commandManager, 'nvim', { get: () => this.nvim }) this.cursors = new Cursors(nvim) listManager.init(nvim) this.addAction('checkJsonExtension', () => { if (extensions.has('coc-json')) return void window.showInformationMessage(`Run :CocInstall coc-json for json intellisense`) }) this.addAction('rootPatterns', (bufnr: number) => this.handler.workspace.getRootPatterns(bufnr)) this.addAction('ensureDocument', (bufnr?: number) => this.handler.workspace.ensureDocument(bufnr)) this.addAction('addWorkspaceFolder', (folder: string) => this.handler.workspace.addWorkspaceFolder(folder)) this.addAction('removeWorkspaceFolder', (folder: string) => this.handler.workspace.removeWorkspaceFolder(folder)) this.addAction('getConfig', (key: string) => this.handler.workspace.getConfiguration(key)) this.addAction('doAutocmd', (id: number, ...args: []) => this.handler.workspace.doAutocmd(id, args)) this.addAction('openLog', () => this.handler.workspace.openLog()) this.addAction('attach', () => workspace.attach()) this.addAction('detach', () => workspace.detach()) this.addAction('doKeymap', (key: string, defaultReturn: string) => this.handler.workspace.doKeymap(key, defaultReturn)) this.addAction('registerExtensions', (...folders: string[]) => extensions.manager.loadExtension(folders), 'registExtensions') this.addAction('snippetCheck', (checkExpand: boolean, checkJump: boolean) => this.handler.workspace.snippetCheck(checkExpand, checkJump)) this.addAction('snippetInsert', (range: Range, newText: string, mode?: InsertTextMode, ultisnip?: UltiSnippetOption) => snippetManager.insertSnippet(newText, true, range, mode, ultisnip)) this.addAction('snippetNext', () => snippetManager.nextPlaceholder()) this.addAction('snippetPrev', () => snippetManager.previousPlaceholder()) this.addAction('snippetCancel', () => snippetManager.cancel()) this.addAction('openLocalConfig', () => this.handler.workspace.openLocalConfig()) this.addAction('bufferCheck', () => this.handler.workspace.bufferCheck()) this.addAction('showInfo', () => this.handler.workspace.showInfo()) this.addAction('hasProvider', (id: string, bufnr?: number) => this.handler.hasProvider(id, bufnr)) this.addAction('cursorsSelect', (bufnr: number, kind: string, mode: string) => this.cursors.select(bufnr, kind, mode)) this.addAction('commandList', () => this.handler.commands.getCommandList()) this.addAction('selectSymbolRange', (inner: boolean, visualmode: string, supportedSymbols: string[]) => this.handler.symbols.selectSymbolRange(inner, visualmode, supportedSymbols)) this.addAction('openList', (...args: string[]) => listManager.start(args)) this.addAction('listNames', () => listManager.names) this.addAction('listDescriptions', () => listManager.descriptions) this.addAction('listLoadItems', (name: string) => listManager.loadItems(name)) this.addAction('listResume', (name?: string) => listManager.resume(name)) this.addAction('listCancel', () => listManager.cancel(true)) this.addAction('listPrev', (name?: string) => listManager.previous(name)) this.addAction('listNext', (name?: string) => listManager.next(name)) this.addAction('listFirst', (name?: string) => listManager.first(name)) this.addAction('listLast', (name?: string) => listManager.last(name)) this.addAction('sendRequest', (id: string, method: string, params?: any) => services.sendRequest(id, method, params)) this.addAction('sendNotification', (id: string, method: string, params?: any) => services.sendNotification(id, method, params)) this.addAction('registerNotification', (id: string, method: string) => services.registerNotification(id, method), 'registNotification') this.addAction('updateConfig', (section: string, val: any) => workspace.configurations.updateMemoryConfig({ [section]: val })) this.addAction('links', () => this.handler.links.getLinks()) this.addAction('openLink', () => this.handler.links.openCurrentLink()) this.addAction('pickColor', () => this.handler.colors.pickColor()) this.addAction('colorPresentation', () => this.handler.colors.pickPresentation()) this.addAction('highlight', () => this.handler.documentHighlighter.highlight()) this.addAction('fold', (kind?: string) => this.handler.fold.fold(kind)) this.addAction('startCompletion', (option: { source?: string, col?: number }) => completion.startCompletion(option)) this.addAction('stopCompletion', (kind: CompleteFinishKind) => completion.stop(kind)) this.addAction('sourceStat', () => sources.sourceStats()) this.addAction('refreshSource', (name: string) => sources.refresh(name)) this.addAction('toggleSource', (name: string) => sources.toggleSource(name)) this.addAction('fillDiagnostics', (bufnr: number) => diagnosticManager.setLocationlist(bufnr)) this.addAction('diagnosticRefresh', (bufnr?: number) => diagnosticManager.refresh(bufnr)) this.addAction('diagnosticInfo', (target?: string) => diagnosticManager.echoCurrentMessage(target)) this.addAction('diagnosticToggle', (enable?: number) => diagnosticManager.toggleDiagnostic(enable)) this.addAction('diagnosticToggleBuffer', (bufnr?: number, enable?: number) => diagnosticManager.toggleDiagnosticBuffer(bufnr, enable)) this.addAction('diagnosticNext', (severity?: string) => diagnosticManager.jumpNext(severity)) this.addAction('diagnosticPrevious', (severity?: string) => diagnosticManager.jumpPrevious(severity)) this.addAction('diagnosticPreview', () => diagnosticManager.preview()) this.addAction('diagnosticList', () => diagnosticManager.getDiagnosticList()) this.addAction('diagnosticRelatedInformation', () => diagnosticManager.relatedInformation()) this.addAction('findLocations', (id: string, method: string, params: any, openCommand: string) => this.handler.locations.findLocations(id, method, params, openCommand)) this.addAction('getTagList', () => this.handler.locations.getTagList()) this.addAction('definitions', () => this.handler.locations.definitions()) this.addAction('declarations', () => this.handler.locations.declarations()) this.addAction('implementations', () => this.handler.locations.implementations()) this.addAction('typeDefinitions', () => this.handler.locations.typeDefinitions()) this.addAction('references', (excludeDeclaration?: boolean) => this.handler.locations.references(excludeDeclaration)) this.addAction('jumpUsed', (openCommand?: string) => this.handler.locations.gotoReferences(openCommand, false)) this.addAction('jumpDefinition', (openCommand?: string | false) => this.handler.locations.gotoDefinition(openCommand)) this.addAction('jumpReferences', (openCommand?: string | false) => this.handler.locations.gotoReferences(openCommand)) this.addAction('jumpTypeDefinition', (openCommand?: string | false) => this.handler.locations.gotoTypeDefinition(openCommand)) this.addAction('jumpDeclaration', (openCommand?: string | false) => this.handler.locations.gotoDeclaration(openCommand)) this.addAction('jumpImplementation', (openCommand?: string | false) => this.handler.locations.gotoImplementation(openCommand)) this.addAction('doHover', (hoverTarget: HoverTarget) => this.handler.hover.onHover(hoverTarget)) this.addAction('definitionHover', (hoverTarget: HoverTarget) => this.handler.hover.definitionHover(hoverTarget)) this.addAction('getHover', (loc?: { bufnr?: number, line: number, col: number }) => this.handler.hover.getHover(loc)) this.addAction('showSignatureHelp', () => this.handler.signature.triggerSignatureHelp()) this.addAction('documentSymbols', (bufnr?: number) => this.handler.symbols.getDocumentSymbols(bufnr)) this.addAction('symbolRanges', () => this.handler.documentHighlighter.getSymbolsRanges()) this.addAction('selectionRanges', () => this.handler.selectionRange.getSelectionRanges()) this.addAction('rangeSelect', (visualmode: string, forward: boolean) => this.handler.selectionRange.selectRange(visualmode, forward)) this.addAction('rename', (newName?: string) => this.handler.rename.rename(newName)) this.addAction('getWorkspaceSymbols', (input: string) => this.handler.symbols.getWorkspaceSymbols(input)) this.addAction('resolveWorkspaceSymbol', (symbolInfo: WorkspaceSymbol) => this.handler.symbols.resolveWorkspaceSymbol(symbolInfo)) this.addAction('formatSelected', (mode: string) => this.handler.format.formatCurrentRange(mode)) this.addAction('format', () => this.handler.format.formatCurrentBuffer()) this.addAction('commands', () => commandManager.commandList) this.addAction('services', () => services.getServiceStats()) this.addAction('toggleService', (name: string) => services.toggle(name)) this.addAction('codeAction', (mode: string | null, only: CodeActionKind[] | string, noExclude: boolean) => this.handler.codeActions.doCodeAction(mode, only, noExclude)) this.addAction('organizeImport', () => this.handler.codeActions.organizeImport()) this.addAction('fixAll', () => this.handler.codeActions.doCodeAction(null, [CodeActionKind.SourceFixAll])) this.addAction('doCodeAction', (codeAction: CodeAction) => this.handler.codeActions.applyCodeAction(codeAction)) this.addAction('codeActions', (mode?: string, only?: CodeActionKind[]) => this.handler.codeActions.getCurrentCodeActions(mode, only)) this.addAction('quickfixes', (mode?: string) => this.handler.codeActions.getCurrentCodeActions(mode, [CodeActionKind.QuickFix])) this.addAction('codeLensAction', () => this.handler.codeLens.doAction()) this.addAction('doQuickfix', () => this.handler.codeActions.doQuickfix()) this.addAction('search', (...args: string[]) => this.handler.refactor.search(args)) this.addAction('saveRefactor', (bufnr: number) => this.handler.refactor.save(bufnr)) this.addAction('refactor', () => this.handler.refactor.doRefactor()) this.addAction('runCommand', (...args: any[]) => this.handler.commands.runCommand(...args)) this.addAction('repeatCommand', () => this.handler.commands.repeat()) this.addAction('installExtensions', (...list: string[]) => extensions.installExtensions(list)) this.addAction('updateExtensions', (silent: boolean) => extensions.updateExtensions(silent, extensions.getUpdateSettings().updateUIInTab)) this.addAction('extensionStats', () => extensions.getExtensionStates()) this.addAction('loadedExtensions', () => extensions.manager.loadedExtensions) this.addAction('watchExtension', (id: string) => extensions.manager.watchExtension(id)) this.addAction('activeExtension', (name: string) => extensions.manager.activate(name)) this.addAction('deactivateExtension', (name: string) => extensions.manager.deactivate(name)) this.addAction('reloadExtension', (name: string) => extensions.manager.reloadExtension(name)) this.addAction('toggleExtension', (name: string) => extensions.manager.toggleExtension(name)) this.addAction('uninstallExtension', (...args: string[]) => extensions.manager.uninstallExtensions(args)) this.addAction('getCurrentFunctionSymbol', () => this.handler.symbols.getCurrentFunctionSymbol()) this.addAction('showOutline', (keep?: number) => this.handler.symbols.showOutline(keep)) this.addAction('hideOutline', () => this.handler.symbols.hideOutline()) this.addAction('getWordEdit', () => this.handler.rename.getWordEdit()) this.addAction('addCommand', (cmd: { id: string, cmd: string, title?: string }) => this.handler.commands.addVimCommand(cmd)) this.addAction('addRanges', (ranges: Range[]) => this.cursors.addRanges(ranges)) this.addAction('currentWorkspacePath', () => workspace.rootPath) this.addAction('selectCurrentPlaceholder', (triggerAutocmd: boolean) => snippetManager.selectCurrentPlaceholder(!!triggerAutocmd)) this.addAction('codeActionRange', (start: number, end: number, only?: string) => this.handler.codeActions.codeActionRange(start, end, only)) this.addAction('incomingCalls', (item?: CallHierarchyItem) => this.handler.callHierarchy.getIncoming(item)) this.addAction('outgoingCalls', (item?: CallHierarchyItem) => this.handler.callHierarchy.getOutgoing(item)) this.addAction('showIncomingCalls', () => this.handler.callHierarchy.showCallHierarchyTree('incoming')) this.addAction('showOutgoingCalls', () => this.handler.callHierarchy.showCallHierarchyTree('outgoing')) this.addAction('showSuperTypes', () => this.handler.typeHierarchy.showTypeHierarchyTree('supertypes')) this.addAction('showSubTypes', () => this.handler.typeHierarchy.showTypeHierarchyTree('subtypes')) this.addAction('inspectSemanticToken', () => this.handler.semanticHighlighter.inspectSemanticToken()) this.addAction('semanticHighlight', () => this.handler.semanticHighlighter.highlightCurrent()) this.addAction('showSemanticHighlightInfo', () => this.handler.semanticHighlighter.showHighlightInfo()) this.addAction('inlineTrigger', (bufnr: number, option?: InlineSuggestOption) => this.handler.inlineCompletion.trigger(bufnr, option)) this.addAction('inlineCancel', () => this.handler.inlineCompletion.cancel()) this.addAction('inlineAccept', (bufnr: number, kind?: AcceptKind) => this.handler.inlineCompletion.accept(bufnr, kind)) this.addAction('inlineNext', (bufnr: number) => this.handler.inlineCompletion.next(bufnr)) this.addAction('inlinePrev', (bufnr: number) => this.handler.inlineCompletion.prev(bufnr)) this.addAction('notificationHistory', () => window.notifications.history) } public get workspace(): Workspace { return workspace } public get window(): Window { return window } public get completion(): Completion { return completion } public addAction(key: string, fn: Callback, alias?: string): void { if (this.actions.has(key)) { throw new Error(`Action ${key} already exists`) } this.actions.set(key, fn) if (alias) this.actions.set(alias, fn) } public async init(rtp: string): Promise { if (this.initialized) return this.initialized = true let { nvim } = this await extensions.init(rtp) await workspace.init(window) nvim.setVar('coc_workspace_initialized', true, true) snippetManager.init() services.init() sources.init() completion.init() diagnosticManager.init() this.handler = new Handler(nvim) this.disposables.push(this.handler) listManager.registerLists() await extensions.activateExtensions() workspace.configurations.flushConfigurations() nvim.pauseNotification() nvim.setVar('coc_service_initialized', 1, true) nvim.call('coc#util#do_autocmd', ['CocNvimInit'], true) nvim.resumeNotification(false, true) logger.info(`coc.nvim initialized with node: ${process.version} after`, Date.now() - getConditionValue(global.__starttime, Date.now())) this.ready = true await events.fire('ready', []) } public get isReady(): boolean { return this.ready } public hasAction(method: string): boolean { return this.actions.has(method) } public async cocAction(method: string, ...args: any[]): Promise { let fn = this.actions.get(method) if (!fn) throw new Error(`Action "${method}" does not exist`) return await Promise.resolve(fn.apply(null, args)) } public getHandler(): Handler { return this.handler } public dispose(): void { disposeAll(this.disposables) extensions.dispose() listManager.dispose() workspace.dispose() window.dispose() sources.dispose() services.dispose() snippetManager.dispose() commandManager.dispose() completion.dispose() diagnosticManager.dispose() } } ================================================ FILE: src/provider/callHierarchyManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Position } from 'vscode-languageserver-types' import { CancellationToken, Disposable } from '../util/protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { CallHierarchyProvider, DocumentSelector } from './index' import Manager from './manager' export default class CallHierarchyManager extends Manager { public register(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): Promise { let item = this.getProvider(document) if (!item) return null let { provider } = item return await Promise.resolve(provider.prepareCallHierarchy(document, position, token)) } public async provideCallHierarchyOutgoingCalls(document: TextDocument, item: CallHierarchyItem, token: CancellationToken): Promise { let providerItem = this.getProvider(document) if (!providerItem) return null let { provider } = providerItem return await Promise.resolve(provider.provideCallHierarchyOutgoingCalls(item, token)) } public async provideCallHierarchyIncomingCalls(document: TextDocument, item: CallHierarchyItem, token: CancellationToken): Promise { let providerItem = this.getProvider(document) if (!providerItem) return null let { provider } = providerItem return await Promise.resolve(provider.provideCallHierarchyIncomingCalls(item, token)) } } ================================================ FILE: src/provider/codeActionManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { CodeAction, CodeActionContext, CodeActionKind, Command, Range } from 'vscode-languageserver-types' import { isFalsyOrEmpty } from '../util/array' import * as Is from '../util/is' import { omit } from '../util/lodash' import { CancellationToken, Disposable } from '../util/protocol' import { CodeActionProvider, DocumentSelector } from './index' import Manager from './manager' interface ProviderMeta { kinds: CodeActionKind[] | undefined clientId: string } /* * With providerId so it can be resolved. */ export interface ExtendedCodeAction extends CodeAction { providerId?: string extensionName?: string } function codeActionContains(kinds: CodeActionKind[], kind: CodeActionKind): boolean { return kinds.some(k => kind === k || kind.startsWith(k + '.')) } export function checkAction(only: CodeActionKind[] | undefined, action: CodeAction | Command): boolean { if (isFalsyOrEmpty(only)) return true if (Command.is(action)) return false return codeActionContains(only, action.kind) } export default class CodeActionManager extends Manager { public register(selector: DocumentSelector, provider: CodeActionProvider, clientId: string | undefined, codeActionKinds?: CodeActionKind[]): Disposable { return this.addProvider({ id: uuid(), selector, provider, kinds: codeActionKinds, clientId }) } public async provideCodeActions( document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken ): Promise { let providers = this.getProviders(document) const only = isFalsyOrEmpty(context.only) ? undefined : context.only if (only) { providers = providers.filter(p => { if (Array.isArray(p.kinds) && !p.kinds.some(kind => codeActionContains(only, kind))) { return false } return true }) } let res: ExtendedCodeAction[] = [] const titles: string[] = [] let results = await Promise.allSettled(providers.map(item => { let { provider, id } = item let fn = async () => { let actions = await Promise.resolve(provider.provideCodeActions(document, range, context, token)) let extensionName = provider['__extensionName'] if (isFalsyOrEmpty(actions)) return for (let action of actions) { if (titles.includes(action.title) || !checkAction(only, action)) continue if (Command.is(action)) { let codeAction: ExtendedCodeAction = { title: action.title, command: action, providerId: id, extensionName, } res.push(codeAction) } else { res.push(Object.assign({ providerId: id }, action)) } titles.push(action.title) } } return fn() })) this.handleResults(results, 'provideCodeActions') return res } public async resolveCodeAction(codeAction: ExtendedCodeAction, token: CancellationToken): Promise { // no need to resolve if (codeAction.edit != null || codeAction.providerId == null) return codeAction let provider = this.getProviderById(codeAction.providerId) if (!provider || !Is.func(provider.resolveCodeAction)) return codeAction let resolved = await Promise.resolve(provider.resolveCodeAction(omit(codeAction, ['providerId']), token)) return resolved ?? codeAction } } ================================================ FILE: src/provider/codeLensManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { CancellationToken, Disposable } from '../util/protocol' import type { CodeLens } from 'vscode-languageserver-types' import { TextDocument } from 'vscode-languageserver-textdocument' import { omit } from '../util/lodash' import { CodeLensProvider, DocumentSelector } from './index' import Manager from './manager' import { isCommand } from '../util/is' interface CodeLensWithSource extends CodeLens { source?: string } export default class CodeLensManager extends Manager { public register(selector: DocumentSelector, provider: CodeLensProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideCodeLenses( document: TextDocument, token: CancellationToken ): Promise { let providers = this.getProviders(document) let codeLens: CodeLens[] = [] let results = await Promise.allSettled(providers.map(item => { let { provider, id } = item return Promise.resolve(provider.provideCodeLenses(document, token)).then(res => { if (Array.isArray(res)) { for (let item of res) { codeLens.push(Object.assign({ source: id }, item)) } } }) })) this.handleResults(results, 'provideCodeLenses') return codeLens } public async resolveCodeLens( codeLens: CodeLensWithSource, token: CancellationToken ): Promise { // no need to resolve if (isCommand(codeLens.command)) return codeLens let provider = this.getProviderById(codeLens.source) if (!provider || typeof provider.resolveCodeLens != 'function') { return codeLens } let res = await Promise.resolve(provider.resolveCodeLens(omit(codeLens, ['source']), token)) Object.assign(codeLens, res) return codeLens } } ================================================ FILE: src/provider/declarationManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position } from 'vscode-languageserver-types' import { LocationWithTarget } from '../types' import { CancellationToken, Disposable } from '../util/protocol' import { DeclarationProvider, DocumentSelector } from './index' import Manager from './manager' export default class DeclarationManager extends Manager { public register(selector: DocumentSelector, provider: DeclarationProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideDeclaration( document: TextDocument, position: Position, token: CancellationToken ): Promise { const providers = this.getProviders(document) let locations: LocationWithTarget[] = [] const results = await Promise.allSettled(providers.map(item => { return Promise.resolve(item.provider.provideDeclaration(document, position, token)).then(location => { this.addLocation(locations, location) }) })) this.handleResults(results, 'provideDeclaration') return locations } } ================================================ FILE: src/provider/definitionManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { DefinitionLink, LocationLink, Position } from 'vscode-languageserver-types' import { LocationWithTarget } from '../types' import { CancellationToken, Disposable } from '../util/protocol' import { DefinitionProvider, DocumentSelector } from './index' import Manager from './manager' export default class DefinitionManager extends Manager { public register(selector: DocumentSelector, provider: DefinitionProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideDefinition( document: TextDocument, position: Position, token: CancellationToken ): Promise { const providers = this.getProviders(document) let locations: LocationWithTarget[] = [] const results = await Promise.allSettled(providers.map(item => { return Promise.resolve(item.provider.provideDefinition(document, position, token)).then(location => { this.addLocation(locations, location) }) })) this.handleResults(results, 'provideDefinition') return locations } public async provideDefinitionLinks( document: TextDocument, position: Position, token: CancellationToken ): Promise { const providers = this.getProviders(document) let locations: DefinitionLink[] = [] const results = await Promise.allSettled(providers.map(item => { return Promise.resolve(item.provider.provideDefinition(document, position, token)).then(location => { if (Array.isArray(location)) { location.forEach(loc => { if (LocationLink.is(loc)) { locations.push(loc) } }) } }) })) this.handleResults(results, 'provideDefinition') return locations } } ================================================ FILE: src/provider/documentColorManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { ColorInformation, ColorPresentation } from 'vscode-languageserver-types' import { equals } from '../util/object' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentColorProvider, DocumentSelector } from './index' import Manager from './manager' interface ColorWithSource extends ColorInformation { source?: string } export default class DocumentColorManager extends Manager { public register(selector: DocumentSelector, provider: DocumentColorProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideDocumentColors(document: TextDocument, token: CancellationToken): Promise { let items = this.getProviders(document) let colors: ColorWithSource[] = [] const results = await Promise.allSettled(items.map(item => { let { id } = item return Promise.resolve(item.provider.provideDocumentColors(document, token)).then(arr => { let noCheck = colors.length == 0 if (Array.isArray(arr)) { for (let color of arr) { if (noCheck || !colors.some(o => equals(o.range, color.range))) { colors.push(Object.assign({ source: id }, color)) } } } }) })) this.handleResults(results, 'provideDocumentColors') return colors } public async provideColorPresentations(colorInformation: ColorWithSource, document: TextDocument, token: CancellationToken): Promise { let providers = this.getProviders(document) let { range, color } = colorInformation for (let item of providers) { let res = await Promise.resolve(item.provider.provideColorPresentations(color, { document, range }, token)) if (res) return res } return null } } ================================================ FILE: src/provider/documentHighlightManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { DocumentHighlight, Position } from 'vscode-languageserver-types' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentHighlightProvider, DocumentSelector } from './index' import Manager from './manager' export default class DocumentHighlightManager extends Manager { public register(selector: DocumentSelector, provider: DocumentHighlightProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideDocumentHighlights( document: TextDocument, position: Position, token: CancellationToken ): Promise { let items = this.getProviders(document) let res: DocumentHighlight[] = null for (const item of items) { try { res = await Promise.resolve(item.provider.provideDocumentHighlights(document, position, token)) if (res != null) break } catch (e) { this.handleResults([{ status: 'rejected', reason: e }], 'provideDocumentHighlights') } } return res } } ================================================ FILE: src/provider/documentLinkManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { DocumentLink, Range } from 'vscode-languageserver-types' import { omit } from '../util/lodash' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentLinkProvider, DocumentSelector } from './index' import Manager from './manager' interface DocumentLinkWithSource extends DocumentLink { source?: string } function rangeToString(range: Range): string { return `${range.start.line},${range.start.character},${range.end.line},${range.end.character}` } export default class DocumentLinkManager extends Manager { public register(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideDocumentLinks(document: TextDocument, token: CancellationToken): Promise { let items = this.getProviders(document) if (items.length == 0) return null const links: DocumentLinkWithSource[] = [] const seenRanges: Set = new Set() const results = await Promise.allSettled(items.map(async item => { let { id, provider } = item const arr = await provider.provideDocumentLinks(document, token) if (Array.isArray(arr)) { let check = links.length > 0 arr.forEach(link => { if (check) { const rangeString = rangeToString(link.range) if (!seenRanges.has(rangeString)) { seenRanges.add(rangeString) links.push(Object.assign({ source: id }, link)) } } else { if (items.length > 1) seenRanges.add(rangeToString(link.range)) links.push(Object.assign({ source: id }, link)) } }) } })) this.handleResults(results, 'provideDocumentLinks') return links } public async resolveDocumentLink(link: DocumentLinkWithSource, token: CancellationToken): Promise { let provider = this.getProviderById(link.source) if (typeof provider.resolveDocumentLink === 'function') { let resolved = await Promise.resolve(provider.resolveDocumentLink(omit(link, ['source']), token)) if (resolved) Object.assign(link, resolved) } return link } } ================================================ FILE: src/provider/documentSymbolManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { DocumentSymbol, SymbolInformation, SymbolTag } from 'vscode-languageserver-types' import { isFalsyOrEmpty, toArray } from '../util/array' import { compareRangesUsingStarts, equalsRange, rangeInRange } from '../util/position' import { CancellationToken, Disposable } from '../util/protocol' import { toText } from '../util/string' import { DocumentSelector, DocumentSymbolProvider, DocumentSymbolProviderMetadata } from './index' import Manager from './manager' export default class DocumentSymbolManager extends Manager { public register(selector: DocumentSelector, provider: DocumentSymbolProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public getMetaData(document: TextDocument): DocumentSymbolProviderMetadata | null { let item = this.getProvider(document) if (!item) return null return item.provider.meta ?? {} } public async provideDocumentSymbols( document: TextDocument, token: CancellationToken ): Promise { let item = this.getProvider(document) if (!item) return null let symbols: DocumentSymbol[] | null = [] let results = await Promise.allSettled([item].map(item => { return Promise.resolve(item.provider.provideDocumentSymbols(document, token)).then(result => { if (!token.isCancellationRequested && !isFalsyOrEmpty(result)) { if (DocumentSymbol.is(result[0])) { symbols = result as DocumentSymbol[] } else { symbols = asDocumentSymbolTree(result as SymbolInformation[]) } } }) })) this.handleResults(results, 'provideDocumentSymbols') return symbols } } export function asDocumentSymbolTree(infos: SymbolInformation[]): DocumentSymbol[] { infos = infos.slice().sort((a, b) => { return compareRangesUsingStarts(a.location.range, b.location.range) }) const res: DocumentSymbol[] = [] const parentStack: DocumentSymbol[] = [] for (const info of infos) { const element: DocumentSymbol = { name: toText(info.name), kind: info.kind, tags: toArray(info.tags), detail: '', range: info.location.range, selectionRange: info.location.range, } if (info.deprecated) { element.tags.push(SymbolTag.Deprecated) } while (true) { if (parentStack.length === 0) { parentStack.push(element) res.push(element) break } const parent = parentStack[parentStack.length - 1] if (rangeInRange(element.range, parent.range) && !equalsRange(parent.range, element.range)) { parent.children = toArray(parent.children) parent.children.push(element) parentStack.push(element) break } parentStack.pop() } } return res } ================================================ FILE: src/provider/foldingRangeManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { FoldingRange } from 'vscode-languageserver-types' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, FoldingContext, FoldingRangeProvider } from './index' import Manager from './manager' export default class FoldingRangeManager extends Manager { public register(selector: DocumentSelector, provider: FoldingRangeProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. * If multiple folding ranges start at the same position, only the range of the first registered provider is used. * If a folding range overlaps with an other range that has a smaller position, it is also ignored. */ public async provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): Promise { let items = this.getProviders(document) let ranges: FoldingRange[] = [] let results = await Promise.allSettled(items.map(item => { return Promise.resolve(item.provider.provideFoldingRanges(document, context, token)).then(res => { if (Array.isArray(res) && res.length > 0) { if (ranges.length == 0) { ranges.push(...res) } else { for (let r of res) { let sp = getParent(r.startLine, ranges) if (sp?.startLine === r.startLine) continue let ep = getParent(r.endLine, ranges) if (sp === ep) { ranges.push(r) } } } ranges.sort((a, b) => a.startLine - b.startLine) } }) })) this.handleResults(results, 'provideFoldingRanges') return ranges } } function getParent(line: number, sortedRanges: FoldingRange[]): FoldingRange | undefined { for (let r of sortedRanges) { if (line >= r.startLine) { if (line <= r.endLine) { return r } else { continue } } else { break } } return undefined } ================================================ FILE: src/provider/formatManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { FormattingOptions, TextEdit } from 'vscode-languageserver-types' import { CancellationToken, Disposable } from '../util/protocol' import { TextDocumentMatch } from '../types' import { DocumentFormattingEditProvider, DocumentSelector } from './index' import Manager from './manager' export default class FormatManager extends Manager { public register(selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority: number): Disposable { return this.addProvider({ id: uuid(), selector, priority, provider }) } public hasFormatProvider(document: TextDocumentMatch): boolean { return this.getFormatProvider(document) != null } public async provideDocumentFormattingEdits( document: TextDocument, options: FormattingOptions, token: CancellationToken ): Promise { let item = this.getFormatProvider(document) if (!item) return null let { provider } = item let res = await Promise.resolve(provider.provideDocumentFormattingEdits(document, options, token)) if (Array.isArray(res)) { Object.defineProperty(res, '__extensionName', { get: () => item.provider['__extensionName'] }) } return res } } ================================================ FILE: src/provider/formatRangeManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver-types' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentRangeFormattingEditProvider, DocumentSelector } from './index' import Manager from './manager' export default class FormatRangeManager extends Manager { public register(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, priority: number): Disposable { return this.addProvider({ id: uuid(), selector, provider, priority }) } /** * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. */ public async provideDocumentRangeFormattingEdits( document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken ): Promise { let item = this.getFormatProvider(document) if (!item) return null let { provider } = item let res = await Promise.resolve(provider.provideDocumentRangeFormattingEdits(document, range, options, token)) if (Array.isArray(res)) { Object.defineProperty(res, '__extensionName', { get: () => item.provider['__extensionName'] }) } return res } } ================================================ FILE: src/provider/hoverManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Hover, Position } from 'vscode-languageserver-types' import { isHover } from '../util/is' import { equals } from '../util/object' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, HoverProvider } from './index' import Manager from './manager' export default class HoverManager extends Manager { public register(selector: DocumentSelector, provider: HoverProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideHover( document: TextDocument, position: Position, token: CancellationToken ): Promise { let items = this.getProviders(document) let hovers: Hover[] = [] let results = await Promise.allSettled(items.map(item => { return Promise.resolve(item.provider.provideHover(document, position, token)).then(hover => { if (!isHover(hover)) return if (hovers.findIndex(o => equals(o.contents, hover.contents)) == -1) { hovers.push(hover) } }) })) this.handleResults(results, 'provideHover') return hovers } } ================================================ FILE: src/provider/implementationManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position } from 'vscode-languageserver-types' import { LocationWithTarget } from '../types' import { CancellationToken, Disposable } from '../util/protocol' import { ImplementationProvider, DocumentSelector } from './index' import Manager from './manager' export default class ImplementationManager extends Manager { public register(selector: DocumentSelector, provider: ImplementationProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideImplementations( document: TextDocument, position: Position, token: CancellationToken ): Promise { const providers = this.getProviders(document) let locations: LocationWithTarget[] = [] const results = await Promise.allSettled(providers.map(item => { return Promise.resolve(item.provider.provideImplementation(document, position, token)).then(location => { this.addLocation(locations, location) }) })) this.handleResults(results, 'provideImplementations') return locations } } ================================================ FILE: src/provider/index.ts ================================================ 'use strict' import type { CallHierarchyIncomingCall, DocumentFilter, CallHierarchyItem, CallHierarchyOutgoingCall, CancellationToken, CodeAction, CodeActionContext, CodeActionKind, CodeLens, Color, ColorInformation, ColorPresentation, Command, CompletionContext, CompletionItem, CompletionList, Definition, DefinitionLink, DocumentDiagnosticReport, DocumentHighlight, DocumentLink, DocumentSymbol, Event, FoldingRange, FormattingOptions, Hover, InlayHint, InlineValue, InlineValueContext, LinkedEditingRanges, Location, Position, PreviousResultId, Range, SelectionRange, SemanticTokens, SemanticTokensDelta, SignatureHelp, SignatureHelpContext, SymbolInformation, TextEdit, TypeHierarchyItem, WorkspaceDiagnosticReport, WorkspaceDiagnosticReportPartialResult, WorkspaceEdit, WorkspaceSymbol, InlineCompletionContext, InlineCompletionItem, InlineCompletionList } from 'vscode-languageserver-protocol' import type { TextDocument } from 'vscode-languageserver-textdocument' import type { URI } from 'vscode-uri' export type DocumentSelector = (string | DocumentFilter)[] /** * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. * * The snippets below are all valid implementations of the [`HoverProvider`](#HoverProvider): * * ```ts * let a: HoverProvider = { * provideHover(doc, pos, token): ProviderResult { * return new Hover('Hello World') * } * } * * let b: HoverProvider = { * provideHover(doc, pos, token): ProviderResult { * return new Promise(resolve => { * resolve(new Hover('Hello World')) * }) * } * } * * let c: HoverProvider = { * provideHover(doc, pos, token): ProviderResult { * return; // undefined * } * } * ``` */ export type ProviderResult = | T | undefined | null | Thenable /** * The completion item provider interface defines the contract between extensions and * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). * * Providers can delay the computation of the [`detail`](#CompletionItem.detail) * and [`documentation`](#CompletionItem.documentation) properties by implementing the * [`resolveCompletionItem`](#CompletionItemProvider.resolveCompletionItem)-function. However, properties that * are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `range`, must * not be changed during resolve. * * Providers are asked for completions either explicitly by a user gesture or -depending on the configuration- * implicitly when typing words or trigger characters. */ export interface CompletionItemProvider { /** * Provide completion items for the given position and document. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @param context How the completion was triggered. * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, context?: CompletionContext ): ProviderResult /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) * or [details](#CompletionItem.detail). * * The editor will only resolve a completion item once. * @param item A completion item currently active in the UI. * @param token A cancellation token. * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ resolveCompletionItem?( item: CompletionItem, token: CancellationToken ): ProviderResult } /** * The inline completion item provider interface defines the contract between extensions and * the inline completion feature. * * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. */ export interface InlineCompletionItemProvider { /** * Provides inline completion items for the given position and document. * If inline completions are enabled, this method will be called whenever the user stopped typing. * It will also be called when the user explicitly triggers inline completions or explicitly asks for the next or previous inline completion. * In that case, all available inline completions should be returned. * `context.triggerKind` can be used to distinguish between these scenarios. * @param document The document inline completions are requested for. * @param position The position inline completions are requested for. * @param context A context object with additional information. * @param token A cancellation token. * @returns An array of completion items or a thenable that resolves to an array of completion items. */ provideInlineCompletionItems( document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken ): ProviderResult } /** * The hover provider interface defines the contract between extensions and * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ export interface HoverProvider { /** * Provide a hover for the given position and document. Multiple hovers at the same * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A hover or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideHover( document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) * and peek definition features. */ export interface DefinitionProvider { /** * Provide the definition of the symbol at the given position and document. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideDefinition( document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) * and peek definition features. */ export interface DeclarationProvider { /** * Provide the declaration of the symbol at the given position and document. */ provideDeclaration(document: TextDocument, position: Position, token: CancellationToken): ProviderResult } /** * The signature help provider interface defines the contract between extensions and * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ export interface SignatureHelpProvider { /** * Provide help for the signature at the given position and document. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return Signature help or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideSignatureHelp( document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext ): ProviderResult } /** * The type definition provider defines the contract between extensions and * the go to type definition feature. */ export interface TypeDefinitionProvider { /** * Provide the type definition of the symbol at the given position and document. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideTypeDefinition( document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * Value-object that contains additional information when * requesting references. */ export interface ReferenceContext { /** * Include the declaration of the current symbol. */ includeDeclaration: boolean } /** * The reference provider interface defines the contract between extensions and * the [find references](https://code.visualstudio.com/docs/editor/editingevolved#_peek)-feature. */ export interface ReferenceProvider { /** * Provide a set of project-wide references for the given position and document. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param context * @param token A cancellation token. * @return An array of locations or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideReferences( document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken ): ProviderResult } /** * Folding context (for future use) */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface FoldingContext {} /** * The folding range provider interface defines the contract between extensions and * [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding) in the editor. */ export interface FoldingRangeProvider { /** * An optional event to signal that the folding ranges from this provider have changed. */ onDidChangeFoldingRanges?: Event /** * Returns a list of folding ranges or null and undefined if the provider * does not want to participate or was cancelled. * @param document The document in which the command was invoked. * @param context Additional context information (for future use) * @param token A cancellation token. */ provideFoldingRanges( document: TextDocument, context: FoldingContext, token: CancellationToken ): ProviderResult } export interface DocumentSymbolProviderMetadata { /** * A human-readable string that is shown when multiple outlines trees show for one document. */ label?: string } /** * The document symbol provider interface defines the contract between extensions and * the [go to symbol](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-symbol)-feature. */ export interface DocumentSymbolProvider { meta?: DocumentSymbolProviderMetadata /** * Provide symbol information for the given document. * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentSymbols( document: TextDocument, token: CancellationToken ): ProviderResult } /** * The implementation provider interface defines the contract between extensions and * the go to implementation feature. */ export interface ImplementationProvider { /** * Provide the implementations of the symbol at the given position and document. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideImplementation( document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The workspace symbol provider interface defines the contract between extensions and * the [symbol search](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name)-feature. */ export interface WorkspaceSymbolProvider { /** * Project-wide search for a symbol matching the given query string. It is up to the provider * how to search given the query string, like substring, indexOf etc. To improve performance implementors can * skip the [location](#SymbolInformation.location) of symbols and implement `resolveWorkspaceSymbol` to do that * later. * * The `query`-parameter should be interpreted in a *relaxed way* as the editor will apply its own highlighting * and scoring on the results. A good rule of thumb is to match case-insensitive and to simply check that the * characters of *query* appear in their order in a candidate symbol. Don't use prefix, substring, or similar * strict matching. * @param query A non-empty query string. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideWorkspaceSymbols( query: string, token: CancellationToken ): ProviderResult<(WorkspaceSymbol & { deprecated?: boolean })[]> /** * Given a symbol fill in its [location](#SymbolInformation.location). This method is called whenever a symbol * is selected in the UI. Providers can implement this method and return incomplete symbols from * [`provideWorkspaceSymbols`](#WorkspaceSymbolProvider.provideWorkspaceSymbols) which often helps to improve * performance. * @param symbol The symbol that is to be resolved. Guaranteed to be an instance of an object returned from an * earlier call to `provideWorkspaceSymbols`. * @param token A cancellation token. * @return The resolved symbol or a thenable that resolves to that. When no result is returned, * the given `symbol` is used. */ resolveWorkspaceSymbol?( symbol: WorkspaceSymbol, token: CancellationToken ): ProviderResult } /** * The rename provider interface defines the contract between extensions and * the [rename](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol)-feature. */ export interface RenameProvider { /** * Provide an edit that describes changes that have to be made to one * or many resources to rename a symbol to a different name. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param newName The new name of the symbol. If the given name is not valid, the provider must return a rejected promise. * @param token A cancellation token. * @return A workspace edit or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideRenameEdits( document: TextDocument, position: Position, newName: string, token: CancellationToken ): ProviderResult /** * Optional function for resolving and validating a position *before* running rename. The result can * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol * which is being renamed - when omitted the text in the returned range is used. * @param document The document in which rename will be invoked. * @param position The position at which rename will be invoked. * @param token A cancellation token. * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. */ prepareRename?( document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ export interface DocumentFormattingEditProvider { /** * Provide formatting edits for a whole document. * @param document The document in which the command was invoked. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentFormattingEdits( document: TextDocument, options: FormattingOptions, token: CancellationToken ): ProviderResult } /** * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ export interface DocumentRangeFormattingEditProvider { /** * Provide formatting edits for a range in a document. * * The given range is a hint and providers can decide to format a smaller * or larger range. Often this is done by adjusting the start and end * of the range to full syntax nodes. * @param document The document in which the command was invoked. * @param range The range which should be formatted. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentRangeFormattingEdits( document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken ): ProviderResult } /** * The code action interface defines the contract between extensions and * the [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. * * A code action can be any command that is [known](#commands.getCommands) to the system. */ export interface CodeActionProvider { /** * Provide commands for the given document and range. * @param document The document in which the command was invoked. * @param range The selector or range for which the command was invoked. This will always be a selection if * there is a currently active editor. * @param context Context carrying additional information. * @param token A cancellation token. * @return An array of commands, quick fixes, or refactorings or a thenable of such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideCodeActions( document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken ): ProviderResult<(Command | CodeAction)[]> /** * Given a code action fill in its [`edit`](#CodeAction.edit)-property. Changes to * all other properties, like title, are ignored. A code action that has an edit * will not be resolved. * @param codeAction A code action. * @param token A cancellation token. * @return The resolved code action or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ resolveCodeAction?(codeAction: T, token: CancellationToken): ProviderResult } /** * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) providers */ export interface CodeActionProviderMetadata { /** * [CodeActionKinds](#CodeActionKind) that this provider may return. * * The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the provider * may list our every specific kind they provide, such as `CodeActionKind.Refactor.Extract.append('function`)` */ readonly providedCodeActionKinds?: ReadonlyArray } /** * The document highlight provider interface defines the contract between extensions and * the word-highlight-feature. */ export interface DocumentHighlightProvider { /** * Provide a set of document highlights, like all occurrences of a variable or * all exit-points of a function. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentHighlights( document: TextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The document link provider defines the contract between extensions and feature of showing * links in the editor. */ export interface DocumentLinkProvider { /** * Provide links for the given document. Note that the editor ships with a default provider that detects * `http(s)` and `file` links. * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult /** * Given a link fill in its [target](#DocumentLink.target). This method is called when an incomplete * link is selected in the UI. Providers can implement this method and return incomple links * (without target) from the [`provideDocumentLinks`](#DocumentLinkProvider.provideDocumentLinks) method which * often helps to improve performance. * @param link The link that is to be resolved. * @param token A cancellation token. */ resolveDocumentLink?(link: DocumentLink, token: CancellationToken): ProviderResult } /** * A code lens provider adds [commands](#Command) to source text. The commands will be shown * as dedicated horizontal lines in between the source text. */ export interface CodeLensProvider { /** * An optional event to signal that the code lenses from this provider have changed. */ onDidChangeCodeLenses?: Event /** * Compute a list of [lenses](#CodeLens). This call should return as fast as possible and if * computing the commands is expensive implementors should only return code lens objects with the * range set and implement [resolve](#CodeLensProvider.resolveCodeLens). * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of code lenses or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult /** * This function will be called for each visible code lens, usually when scrolling and after * calls to [compute](#CodeLensProvider.provideCodeLenses)-lenses. * @param codeLens code lens that must be resolved. * @param token A cancellation token. * @return The given, resolved code lens or thenable that resolves to such. */ resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult } /** * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ export interface OnTypeFormattingEditProvider { /** * Provide formatting edits after a character has been typed. * * The given position and character should hint to the provider * what range the position to expand to, like find the matching `{` * when `}` has been entered. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param ch The character that has been typed. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): ProviderResult } /** * The document color provider defines the contract between extensions and feature of * picking and modifying colors in the editor. */ export interface DocumentColorProvider { /** * Provide colors for the given document. * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of [color information](#ColorInformation) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentColors(document: TextDocument, token: CancellationToken): ProviderResult /** * Provide [representations](#ColorPresentation) for a color. * @param color The color to show and insert. * @param context A context object with additional information * @param token A cancellation token. * @return An array of color presentations or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideColorPresentations(color: Color, context: { document: TextDocument; range: Range }, token: CancellationToken): ProviderResult } export interface TextDocumentContentProvider { /** * An event to signal a resource has changed. */ onDidChange?: Event /** * Provide textual content for a given uri. * * The editor will use the returned string-content to create a readonly * [document](#TextDocument). Resources allocated should be released when * the corresponding document has been [closed](#workspace.onDidCloseTextDocument). * @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for. * @param token A cancellation token. * @return A string or a thenable that resolves to such. */ provideTextDocumentContent(uri: URI, token: CancellationToken): ProviderResult } export interface SelectionRangeProvider { /** * Provide selection ranges starting at a given position. The first range must [contain](#Range.contains) * position and subsequent ranges must contain the previous range. */ provideSelectionRanges(document: TextDocument, positions: Position[], token: CancellationToken): ProviderResult } /** * The call hierarchy provider interface describes the contract between extensions * and the call hierarchy feature which allows to browse calls and caller of function, * methods, constructor etc. */ export interface CallHierarchyProvider { /** * Bootstraps call hierarchy by returning the item that is denoted by the given document * and position. This item will be used as entry into the call graph. Providers should * return `undefined` or `null` when there is no item at the given location. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @returns A call hierarchy item or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult /** * Provide all incoming calls for an item, e.g all callers for a method. In graph terms this describes directed * and annotated edges inside the call graph, e.g the given item is the starting node and the result is the nodes * that can be reached. * @param item The hierarchy item for which incoming calls should be computed. * @param token A cancellation token. * @returns A set of incoming calls or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideCallHierarchyIncomingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult /** * Provide all outgoing calls for an item, e.g call calls to functions, methods, or constructors from the given item. In * graph terms this describes directed and annotated edges inside the call graph, e.g the given item is the starting * node and the result is the nodes that can be reached. * @param item The hierarchy item for which outgoing calls should be computed. * @param token A cancellation token. * @returns A set of outgoing calls or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult } /** * The document semantic tokens provider interface defines the contract between extensions and * semantic tokens. */ export interface DocumentSemanticTokensProvider { /** * An optional event to signal that the semantic tokens from this provider have changed. */ onDidChangeSemanticTokens?: Event /** * Tokens in a file are represented as an array of integers. The position of each token is expressed relative to * the token before it, because most tokens remain stable relative to each other when edits are made in a file. * * --- * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices: * * - at index `5*i` - `deltaLine`: token line number, relative to the previous token * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line) * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline. * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes`. We currently ask that `tokenType` < 65536. * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers` * * --- * ### How to encode tokens * * Here is an example for encoding a file with 3 tokens in a uint32 array: * ``` * { line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, * { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, * { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } * ``` * * 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types. * For this example, we will choose the following legend which must be passed in when registering the provider: * ``` * tokenTypes: ['property', 'type', 'class'], * tokenModifiers: ['private', 'static'] * ``` * * 2. The first transformation step is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked * up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags, * so a `tokenModifier` value of `3` is first viewed as binary `0b00000011`, which means `[tokenModifiers[0], tokenModifiers[1]]` because * bits 0 and 1 are set. Using this legend, the tokens now are: * ``` * { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, * { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 }, * { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } * ``` * * 3. The next step is to represent each token relative to the previous token in the file. In this case, the second token * is on the same line as the first token, so the `startChar` of the second token is made relative to the `startChar` * of the first token, so it will be `10 - 5`. The third token is on a different line than the second token, so the * `startChar` of the third token will not be altered: * ``` * { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, * { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 }, * { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } * ``` * * 4. Finally, the last step is to inline each of the 5 fields for a token in a single array, which is a memory friendly representation: * ``` * // 1st token, 2nd token, 3rd token * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * @see [SemanticTokensBuilder](#SemanticTokensBuilder) for a helper to encode tokens as integers. * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. */ provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult /** * Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement * this method (`provideDocumentSemanticTokensEdits`) and then return incremental updates to the previously provided semantic tokens. * * --- * ### How tokens change when the document changes * * Suppose that `provideDocumentSemanticTokens` has previously returned the following semantic tokens: * ``` * // 1st token, 2nd token, 3rd token * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * * Also suppose that after some edits, the new semantic tokens in a file are: * ``` * // 1st token, 2nd token, 3rd token * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * It is possible to express these new tokens in terms of an edit applied to the previous tokens: * ``` * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens * * edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3 * ``` * * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again. * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. */ provideDocumentSemanticTokensEdits?(document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult } /** * The document range semantic tokens provider interface defines the contract between extensions and * semantic tokens. */ export interface DocumentRangeSemanticTokensProvider { /** * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). */ provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult } export interface LinkedEditingRangeProvider { /** * For a given position in a document, returns the range of the symbol at the position and all ranges * that have the same content. A change to one of the ranges can be applied to all other ranges if the new content * is valid. An optional word pattern can be returned with the result to describe valid contents. * If no result-specific word pattern is provided, the word pattern from the language configuration is used. * @param document The document in which the provider was invoked. * @param position The position at which the provider was invoked. * @param token A cancellation token. * @return A list of ranges that can be edited together */ provideLinkedEditingRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult } /** * The inlay hints provider interface defines the contract between extensions and * the inlay hints feature. */ export interface InlayHintsProvider { /** * An optional event to signal that inlay hints from this provider have changed. */ onDidChangeInlayHints?: Event /** * Provide inlay hints for the given range and document. * * *Note* that inlay hints that are not {@link Range.contains contained} by the given range are ignored. * @param document The document in which the command was invoked. * @param range The range for which inlay hints should be computed. * @param token A cancellation token. * @return An array of inlay hints or a thenable that resolves to such. */ provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): ProviderResult /** * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.textEdits text edits}, * or complete label {@link InlayHintLabelPart parts}. * * *Note* that the editor will resolve an inlay hint at most once. * @param hint An inlay hint. * @param token A cancellation token. * @return The resolved inlay hint or a thenable that resolves to such. It is OK to return the given `item`. When no result is returned, the given `item` will be used. */ resolveInlayHint?(hint: T, token: CancellationToken): ProviderResult } /** * The type hierarchy provider interface describes the contract between extensions * and the type hierarchy feature. */ export interface TypeHierarchyProvider { /** * Bootstraps type hierarchy by returning the item that is denoted by the given document * and position. This item will be used as entry into the type graph. Providers should * return `undefined` or `null` when there is no item at the given location. * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @returns One or multiple type hierarchy items or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ prepareTypeHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult /** * Provide all supertypes for an item, e.g all types from which a type is derived/inherited. In graph terms this describes directed * and annotated edges inside the type graph, e.g the given item is the starting node and the result is the nodes * that can be reached. * @param item The hierarchy item for which super types should be computed. * @param token A cancellation token. * @returns A set of direct supertypes or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideTypeHierarchySupertypes(item: TypeHierarchyItem, token: CancellationToken): ProviderResult /** * Provide all subtypes for an item, e.g all types which are derived/inherited from the given item. In * graph terms this describes directed and annotated edges inside the type graph, e.g the given item is the starting * node and the result is the nodes that can be reached. * @param item The hierarchy item for which subtypes should be computed. * @param token A cancellation token. * @returns A set of direct subtypes or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideTypeHierarchySubtypes(item: TypeHierarchyItem, token: CancellationToken): ProviderResult } /** * The inline values provider interface defines the contract between extensions and the editor's debugger inline values feature. * In this contract the provider returns inline value information for a given document range * and the editor shows this information in the editor at the end of lines. */ export interface InlineValuesProvider { /** * An optional event to signal that inline values have changed. * @see {@link EventEmitter} */ onDidChangeInlineValues?: Event | undefined /** * Provide "inline value" information for a given document and range. * The editor calls this method whenever debugging stops in the given document. * The returned inline values information is rendered in the editor at the end of lines. * @param document The document for which the inline values information is needed. * @param viewPort The visible document range for which inline values should be computed. * @param context A bag containing contextual information like the current location. * @param token A cancellation token. * @return An array of InlineValueDescriptors or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): ProviderResult } export interface ResultReporter { (chunk: WorkspaceDiagnosticReportPartialResult | null): void } export interface DiagnosticProvider { onDidChangeDiagnostics: Event | undefined provideDiagnostics(document: TextDocument | URI, previousResultId: string | undefined, token: CancellationToken): ProviderResult provideWorkspaceDiagnostics?(resultIds: PreviousResultId[], token: CancellationToken, resultReporter: ResultReporter): ProviderResult } ================================================ FILE: src/provider/inlayHintManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { InlayHint, Position, Range } from 'vscode-languageserver-types' import { createLogger } from '../logger' import { comparePosition, positionInRange } from '../util/position' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, InlayHintsProvider } from './index' import Manager from './manager' const logger = createLogger('inlayHintManger') export interface InlayHintWithProvider extends InlayHint { providerId: string resolved?: boolean } export default class InlayHintManger extends Manager { public register(selector: DocumentSelector, provider: InlayHintsProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. */ public async provideInlayHints( document: TextDocument, range: Range, token: CancellationToken ): Promise { let items = this.getProviders(document) let inlayHints: InlayHintWithProvider[] = [] let results = await Promise.allSettled(items.map(async item => { let { id, provider } = item let hints = await Promise.resolve(provider.provideInlayHints(document, range, token)) if (!Array.isArray(hints) || token.isCancellationRequested) return let noCheck = inlayHints.length == 0 for (let hint of hints) { if (!isValidInlayHint(hint, range)) continue if (!noCheck && inlayHints.findIndex(o => sameHint(o, hint)) != -1) continue inlayHints.push({ providerId: id, ...hint }) } })) this.handleResults(results, 'provideInlayHints', token) return inlayHints } public async resolveInlayHint(hint: InlayHintWithProvider, token: CancellationToken): Promise { let provider = this.getProviderById(hint.providerId) if (!provider || typeof provider.resolveInlayHint !== 'function' || hint.resolved === true) return hint let res = await Promise.resolve(provider.resolveInlayHint(hint, token)) if (token.isCancellationRequested) return hint return Object.assign(hint, res, { resolved: true }) } } export function sameHint(one: InlayHint, other: InlayHint): boolean { if (comparePosition(one.position, other.position) !== 0) return false return getLabel(one) === getLabel(other) } export function isInlayHint(obj: any): obj is InlayHint { if (!obj || !Position.is(obj.position) || obj.label == null) return false if (typeof obj.label !== 'string') return Array.isArray(obj.label) && obj.label.every(p => typeof p.value === 'string') return true } export function isValidInlayHint(hint: InlayHint, range: Range): boolean { if (hint.label.length === 0 || (Array.isArray(hint.label) && hint.label.every(part => part.value.length === 0))) { logger.warn('INVALID inlay hint, empty label', hint) return false } if (!isInlayHint(hint)) { logger.warn('INVALID inlay hint', hint) return false } if (range && positionInRange(hint.position, range) !== 0) { // console.log('INVALID inlay hint, position outside range', range, hint); return false } return true } export function getLabel(hint: InlayHint): string { if (typeof hint.label === 'string') return hint.label return hint.label.map(o => o.value).join('') } ================================================ FILE: src/provider/inlineCompletionItemManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { InlineCompletionContext, InlineCompletionItem, Position } from 'vscode-languageserver-types' import { toArray } from '../util/array' import { onUnexpectedError } from '../util/errors' import { omit } from '../util/lodash' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, InlineCompletionItemProvider } from './index' import Manager, { ProviderItem } from './manager' export interface ExtendedInlineContext extends InlineCompletionContext { provider?: string } export default class InlineCompletionItemManager extends Manager { public register(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public get isEmpty(): boolean { return this.providers.size === 0 } /** * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. */ public async provideInlineCompletionItems( document: TextDocument, position: Position, context: ExtendedInlineContext, token: CancellationToken ): Promise { let providers: ProviderItem[] = [] if (context.provider) { let item = this.getProvideByExtension(document, context.provider) if (item) providers = [item] } else { providers = this.getProviders(document) } const items: InlineCompletionItem[] = [] const promise = Promise.allSettled(providers.map(item => { let provider = item.provider return Promise.resolve(provider.provideInlineCompletionItems(document, position, omit(context, ['provider']), token)).then(result => { let list = Array.isArray(result) ? result : toArray(result?.items) for (let item of list) { Object.defineProperty(item, 'provider', { get: () => provider['__extensionName'], enumerable: false }) items.push(item) } }) })) let disposable: Disposable await Promise.race([new Promise(resolve => { disposable = token.onCancellationRequested(() => { resolve(undefined) }) }), promise.then(results => { if (!token.isCancellationRequested) this.handleResults(results, 'provideInlineCompletionItems') })]).catch(onUnexpectedError) if (disposable) disposable.dispose() return items } } ================================================ FILE: src/provider/inlineValueManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { InlineValue, InlineValueContext, Range } from 'vscode-languageserver-types' import { equals } from '../util/object' import { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, InlineValuesProvider } from './index' import Manager from './manager' export default class InlineValueManager extends Manager { public register(selector: DocumentSelector, provider: InlineValuesProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. */ public async provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): Promise { const items = this.getProviders(document) const values: InlineValue[] = [] const results = await Promise.allSettled(items.map(item => { return Promise.resolve(item.provider.provideInlineValues(document, viewPort, context, token)).then(arr => { if (!Array.isArray(arr)) return let noCheck = values.length === 0 for (let value of arr) { if (noCheck || values.every(o => !equals(o, value))) { values.push(value) } } }) })) this.handleResults(results, 'provideInlineValues') return values } } ================================================ FILE: src/provider/linkedEditingRangeManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import type { CancellationToken, Disposable, DocumentSelector, LinkedEditingRanges } from 'vscode-languageserver-protocol' import type { TextDocument } from 'vscode-languageserver-textdocument' import type { Position } from 'vscode-languageserver-types' import { createLogger } from '../logger' import { LinkedEditingRangeProvider } from './index' import Manager from './manager' const logger = createLogger('linkedEditingManager') export default class LinkedEditingRangeManager extends Manager { public register(selector: DocumentSelector, provider: LinkedEditingRangeProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link workspace.match score} and the best-matching provider that has a result is used. Failure * of the selected provider will cause a failure of the whole operation. */ public async provideLinkedEditingRanges(document: TextDocument, position: Position, token: CancellationToken): Promise { let items = this.getProviders(document) for (let item of items) { let res = await Promise.resolve(item.provider.provideLinkedEditingRanges(document, position, token)) if (res != null) return res } return null } } ================================================ FILE: src/provider/manager.ts ================================================ 'use strict' import { Location, LocationLink } from 'vscode-languageserver-types' import { createLogger } from '../logger' import { LocationWithTarget, TextDocumentMatch } from '../types' import { isCancellationError, shouldIgnore } from '../util/errors' import { parseExtensionName } from '../util/extensionRegistry' import { equals } from '../util/object' import { CancellationToken, Disposable } from '../util/protocol' import { toText } from '../util/string' import workspace from '../workspace' import { DocumentSelector } from './index' const logger = createLogger('provider-manager') export type ProviderItem = { id: string selector: DocumentSelector provider: T priority?: number } & P export default class Manager { protected providers: Set> = new Set() public hasProvider(document: TextDocumentMatch): boolean { return this.getProvider(document) != null } protected addProvider(item: ProviderItem): Disposable { if (!item.provider.hasOwnProperty('__extensionName')) { Error.captureStackTrace(item) let name: string Object.defineProperty(item.provider, '__extensionName', { get: () => { if (name) return name name = parseExtensionName(toText(item['stack'])) return name }, enumerable: true }) } this.providers.add(item) return Disposable.create(() => { this.providers.delete(item) }) } protected handleResults(results: PromiseSettledResult[], name: string, token?: CancellationToken): void { let serverCancelError: Error results.forEach(res => { if (res.status === 'rejected') { if (!shouldIgnore(res.reason)) logger.error(`Provider error on ${name}:`, res.reason) if (token && !token.isCancellationRequested && isCancellationError(res.reason)) { serverCancelError = res.reason } } }) if (serverCancelError) throw serverCancelError } protected getProvider(document: TextDocumentMatch): ProviderItem { let currScore = 0 let providerItem: ProviderItem for (let item of this.providers) { let { selector, priority } = item let score = workspace.match(selector, document) if (score == 0) continue if (typeof priority == 'number' && priority > 0) { score = score + priority } if (score < currScore) continue currScore = score providerItem = item } return providerItem } public getProvideByExtension(document: TextDocumentMatch, extension: string): ProviderItem { for (let item of this.providers) { if (item.provider['__extensionName'] === extension) { return item } } logger.warn(`User-specified formatter not found for ${document.languageId}:`, extension) return undefined } protected getFormatProvider(document: TextDocumentMatch): ProviderItem { // Prefer user choice const userChoice = workspace.getConfiguration('coc.preferences', document).get('formatterExtension') if (userChoice) { let provider = this.getProvideByExtension(document, userChoice) if (provider) return provider } return this.getProvider(document) } protected getProviderById(id: string): T { let item = Array.from(this.providers).find(o => o.id == id) return item ? item.provider : null } public getProviders(document: TextDocumentMatch): ProviderItem[] { let items = Array.from(this.providers) items = items.filter(item => workspace.match(item.selector, document) > 0) return items.sort((a, b) => workspace.match(b.selector, document) - workspace.match(a.selector, document)) } public addLocation(locations: LocationWithTarget[], location: Location | Location[] | LocationLink[] | null | undefined): void { if (Array.isArray(location)) { for (let loc of location) { if (Location.is(loc)) { addLocation(locations, loc) } else if (loc && typeof loc.targetUri === 'string') { addLocation(locations, loc) } } } else if (Location.is(location)) { addLocation(locations, location) } } } /** * Add unique location */ function addLocation(arr: LocationWithTarget[], location: Location | LocationLink): void { if (Location.is(location)) { let { range, uri } = location if (arr.find(o => o.uri == uri && equals(o.range, range)) != null) return arr.push(location) } else if (location && typeof location.targetUri === 'string') { let { targetUri, targetSelectionRange, targetRange } = location if (arr.find(o => o.uri == targetUri && equals(o.range, targetSelectionRange)) != null) return arr.push({ uri: targetUri, range: targetSelectionRange, targetRange }) } } ================================================ FILE: src/provider/onTypeFormatManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, TextEdit } from 'vscode-languageserver-types' import { CancellationToken, Disposable } from '../util/protocol' import workspace from '../workspace' import type { OnTypeFormattingEditProvider, DocumentSelector } from './index' import Manager from './manager' export interface ProviderMeta { triggerCharacters: string[] } export default class OnTypeFormatManager extends Manager { public register(selector: DocumentSelector, provider: OnTypeFormattingEditProvider, triggerCharacters: string[] | undefined): Disposable { return this.addProvider({ id: uuid(), selector, provider, triggerCharacters: triggerCharacters ?? [] }) } public couldTrigger(document: TextDocument, triggerCharacter: string): OnTypeFormattingEditProvider | null { for (let o of this.providers) { let { triggerCharacters, selector } = o if (workspace.match(selector, document) > 0 && triggerCharacters.includes(triggerCharacter)) { return o.provider } } return null } public async onCharacterType(character: string, document: TextDocument, position: Position, token: CancellationToken): Promise { let items = this.getProviders(document) let item = items.find(o => o.triggerCharacters.includes(character)) if (!item) return null let formatOpts = await workspace.getFormatOptions(document.uri) return await Promise.resolve(item.provider.provideOnTypeFormattingEdits(document, position, character, formatOpts, token)) } } ================================================ FILE: src/provider/referenceManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import type { CancellationToken, Disposable, DocumentSelector, Position, ReferenceContext } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { LocationWithTarget } from '../types' import type { ReferenceProvider } from './index' import Manager from './manager' export default class ReferenceManager extends Manager { public register(selector: DocumentSelector, provider: ReferenceProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideReferences( document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken ): Promise { const providers = this.getProviders(document) let locations: LocationWithTarget[] = [] const results = await Promise.allSettled(providers.map(item => { return Promise.resolve(item.provider.provideReferences(document, position, context, token)).then(location => { this.addLocation(locations, location) }) })) this.handleResults(results, 'provideReferences') return locations } } ================================================ FILE: src/provider/renameManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, Range, WorkspaceEdit } from 'vscode-languageserver-types' import type { CancellationToken, Disposable } from '../util/protocol' import { RenameProvider, DocumentSelector } from './index' import Manager from './manager' export default class RenameManager extends Manager { public register(selector: DocumentSelector, provider: RenameProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link workspace.match score} and asked in sequence. The first provider producing a result * defines the result of the whole operation. */ public async provideRenameEdits( document: TextDocument, position: Position, newName: string, token: CancellationToken ): Promise { let items = this.getProviders(document) let edit: WorkspaceEdit = null for (const item of items) { try { edit = await Promise.resolve(item.provider.provideRenameEdits(document, position, newName, token)) } catch (e) { this.handleResults([{ status: 'rejected', reason: e }], 'provideRenameEdits') } if (edit != null) break } return edit } public async prepareRename( document: TextDocument, position: Position, token: CancellationToken ): Promise { let items = this.getProviders(document) items = items.filter(o => typeof o.provider.prepareRename === 'function') if (items.length === 0) return null for (const item of items) { let res = await Promise.resolve(item.provider.prepareRename(document, position, token)) // can rename if (res != null) return res } return false } } ================================================ FILE: src/provider/selectionRangeManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, SelectionRange } from 'vscode-languageserver-types' import { equals } from '../util/object' import { rangeInRange } from '../util/position' import type { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, SelectionRangeProvider } from './index' import Manager from './manager' export default class SelectionRangeManager extends Manager { public register(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. */ public async provideSelectionRanges( document: TextDocument, positions: Position[], token: CancellationToken ): Promise { let items = this.getProviders(document) if (items.length === 0) return null let selectionRangeResult: SelectionRange[][] = [] let results = await Promise.allSettled(items.map(item => { return Promise.resolve(item.provider.provideSelectionRanges(document, positions, token)).then(ranges => { if (Array.isArray(ranges) && ranges.length > 0) { selectionRangeResult.push(ranges) } }) })) this.handleResults(results, 'provideSelectionRanges') if (selectionRangeResult.length === 0) return null let selectionRanges = selectionRangeResult[0] // concat ranges when possible if (selectionRangeResult.length > 1) { for (let i = 1; i <= selectionRangeResult.length - 1; i++) { let start = selectionRanges[0].range let end = selectionRanges[selectionRanges.length - 1].range let ranges = selectionRangeResult[i] let len = ranges.length if (rangeInRange(end, ranges[0].range) && !equals(end, ranges[0].range)) { selectionRanges.push(...ranges) } else if (rangeInRange(ranges[len - 1].range, start) && !equals(ranges[len - 1].range, start)) { selectionRanges.unshift(...ranges) } } } for (let i = 0; i < selectionRanges.length - 1; i++) { let r = selectionRanges[i] r.parent = selectionRanges[i + 1] } return selectionRanges } } ================================================ FILE: src/provider/semanticTokensManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { SemanticTokens, SemanticTokensDelta, SemanticTokensLegend } from 'vscode-languageserver-types' import type { CancellationToken, Disposable } from '../util/protocol' import { DocumentSemanticTokensProvider, DocumentSelector } from './index' import Manager from './manager' interface ProviderMeta { legend: SemanticTokensLegend } export default class SemanticTokensManager extends Manager { public register(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable { return this.addProvider({ id: uuid(), selector, provider, legend, }) } public getLegend(document: TextDocument): SemanticTokensLegend { const item = this.getProvider(document) if (!item) return return item.legend } public hasSemanticTokensEdits(document: TextDocument): boolean { let provider = this.getProvider(document)?.provider if (!provider) return false return (typeof provider.provideDocumentSemanticTokensEdits === 'function') } public async provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): Promise { let provider = this.getProvider(document)?.provider if (!provider || typeof provider.provideDocumentSemanticTokens !== 'function') return null return await Promise.resolve(provider.provideDocumentSemanticTokens(document, token)) } public async provideDocumentSemanticTokensEdits(document: TextDocument, previousResultId: string, token: CancellationToken): Promise { let item = this.getProvider(document) if (!item || typeof item.provider.provideDocumentSemanticTokensEdits !== 'function') return null return await Promise.resolve(item.provider.provideDocumentSemanticTokensEdits(document, previousResultId, token)) } } ================================================ FILE: src/provider/semanticTokensRangeManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Range, SemanticTokens, SemanticTokensLegend } from 'vscode-languageserver-types' import type { CancellationToken, Disposable } from '../util/protocol' import { DocumentRangeSemanticTokensProvider, DocumentSelector } from './index' import Manager from './manager' interface ProviderMeta { legend: SemanticTokensLegend } export default class SemanticTokensRangeManager extends Manager { public register(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable { return this.addProvider({ id: uuid(), selector, legend, provider }) } public getLegend(document: TextDocument): SemanticTokensLegend { const item = this.getProvider(document) if (!item) return return item.legend } public async provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): Promise { let item = this.getProvider(document) if (!item) return null let { provider } = item return await Promise.resolve(provider.provideDocumentRangeSemanticTokens(document, range, token)) } } ================================================ FILE: src/provider/signatureManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import type { SignatureHelpContext } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, SignatureHelp } from 'vscode-languageserver-types' import { isFalsyOrEmpty } from '../util/array' import type { CancellationToken, Disposable } from '../util/protocol' import { DocumentSelector, SignatureHelpProvider } from './index' import Manager from './manager' interface ProviderMeta { triggerCharacters: string[] | undefined } export default class SignatureManager extends Manager { public register(selector: DocumentSelector, provider: SignatureHelpProvider, triggerCharacters: string[] | undefined): Disposable { triggerCharacters = isFalsyOrEmpty(triggerCharacters) ? [] : triggerCharacters let characters = triggerCharacters.reduce((p, c) => p.concat(c.length == 1 ? [c] : c.split(/\s*/g)), [] as string[]) return this.addProvider({ id: uuid(), selector, provider, triggerCharacters: characters }) } public shouldTrigger(document: TextDocument, triggerCharacter: string): boolean { let items = this.getProviders(document) if (items.length === 0) return false for (let item of items) { if (item.triggerCharacters.includes(triggerCharacter)) { return true } } return false } /** * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link languages.match score} and called sequentially until a provider returns a * valid result. */ public async provideSignatureHelp( document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext ): Promise { let items = this.getProviders(document) for (const item of items) { let res = await Promise.resolve(item.provider.provideSignatureHelp(document, position, token, context)) if (res && res.signatures && res.signatures.length > 0) return res } return null } } ================================================ FILE: src/provider/typeDefinitionManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position } from 'vscode-languageserver-types' import { LocationWithTarget } from '../types' import type { CancellationToken, Disposable } from '../util/protocol' import { TypeDefinitionProvider, DocumentSelector } from './index' import Manager from './manager' export default class TypeDefinitionManager extends Manager { public register(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } public async provideTypeDefinition( document: TextDocument, position: Position, token: CancellationToken ): Promise { const providers = this.getProviders(document) let locations: LocationWithTarget[] = [] const results = await Promise.allSettled(providers.map(item => { return Promise.resolve(item.provider.provideTypeDefinition(document, position, token)).then(location => { this.addLocation(locations, location) }) })) this.handleResults(results, 'provideTypeDefinition') return locations } } ================================================ FILE: src/provider/typeHierarchyManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { TextDocument } from 'vscode-languageserver-textdocument' import { Position, TypeHierarchyItem } from 'vscode-languageserver-types' import { omit } from '../util/lodash' import { CancellationToken, Disposable } from '../util/protocol' import { TypeHierarchyProvider, DocumentSelector } from './index' import Manager from './manager' export interface TypeHierarchyItemWithSource extends TypeHierarchyItem { source?: string } const excludeKeys = ['source'] export default class TypeHierarchyManager extends Manager { public register(selector: DocumentSelector, provider: TypeHierarchyProvider): Disposable { return this.addProvider({ id: uuid(), selector, provider }) } /** * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. */ public async prepareTypeHierarchy(document: TextDocument, position: Position, token: CancellationToken): Promise { const items = this.getProviders(document) let hierarchyItems: TypeHierarchyItemWithSource[] = [] let results = await Promise.allSettled(items.map(item => { let { provider, id } = item return (async () => { let arr = await Promise.resolve(provider.prepareTypeHierarchy(document, position, token)) if (Array.isArray(arr)) { let noCheck = hierarchyItems.length === 0 arr.forEach(item => { if (noCheck || hierarchyItems.every(o => o.name !== item.name)) { hierarchyItems.push(Object.assign({ source: id }, item)) } }) } })() })) this.handleResults(results, 'prepareTypeHierarchy') return hierarchyItems } public async provideTypeHierarchySupertypes(item: TypeHierarchyItemWithSource, token: CancellationToken): Promise { let { source } = item const provider = this.getProviderById(source) if (!provider) return [] return await Promise.resolve(provider.provideTypeHierarchySupertypes(omit(item, excludeKeys), token)).then(arr => { if (Array.isArray(arr)) { return arr.map(item => { return Object.assign({ source }, item) }) } return [] }) } public async provideTypeHierarchySubtypes(item: TypeHierarchyItemWithSource, token: CancellationToken): Promise { let { source } = item const provider = this.getProviderById(source) if (!provider) return [] return await Promise.resolve(provider.provideTypeHierarchySubtypes(omit(item, excludeKeys), token)).then(arr => { if (Array.isArray(arr)) { return arr.map(item => { return Object.assign({ source }, item) }) } return [] }) } } ================================================ FILE: src/provider/workspaceSymbolsManager.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { WorkspaceSymbol } from 'vscode-languageserver-types' import type { CancellationToken, Disposable } from '../util/protocol' import { WorkspaceSymbolProvider } from './index' import Manager from './manager' interface WorkspaceSymbolWithSource extends WorkspaceSymbol { source?: string } export default class WorkspaceSymbolManager extends Manager { public register(provider: WorkspaceSymbolProvider): Disposable { return this.addProvider({ id: uuid(), selector: [{ language: '*' }], provider }) } public async provideWorkspaceSymbols( query: string, token: CancellationToken ): Promise { let entries = Array.from(this.providers) let infos: WorkspaceSymbol[] = [] let results = await Promise.allSettled(entries.map(o => { let { id, provider } = o return Promise.resolve(provider.provideWorkspaceSymbols(query, token)).then(list => { if (Array.isArray(list)) { infos.push(...list.map(item => Object.assign({ source: id }, item))) } }) })) this.handleResults(results, 'provideWorkspaceSymbols') return infos } public async resolveWorkspaceSymbol( symbolInfo: WorkspaceSymbolWithSource, token: CancellationToken ): Promise { let provider = this.getProviderById(symbolInfo.source) if (!provider || typeof provider.resolveWorkspaceSymbol !== 'function') return symbolInfo return provider.resolveWorkspaceSymbol(symbolInfo, token) } public hasProvider(): boolean { return this.providers.size > 0 } } ================================================ FILE: src/services.ts ================================================ 'use strict' import { SpawnOptions } from 'child_process' import type { DocumentSelector } from 'vscode-languageserver-protocol' import type { TextDocument } from 'vscode-languageserver-textdocument' import type { WorkspaceFolder } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import events from './events' import { Executable, ForkOptions, LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions, State, Transport, TransportKind } from './language-client' import { createLogger } from './logger' import { disposeAll, wait } from './util' import { toArray } from './util/array' import { fs, net, path } from './util/node' import { toObject } from './util/object' import { CancellationToken, Disposable, Emitter, Event } from './util/protocol' import window from './window' import workspace from './workspace' const logger = createLogger('services') export enum ServiceStat { Initial, Starting, StartFailed, Running, Stopping, Stopped, } interface ServiceInfo { id: string state: string languageIds: string[] } export interface LanguageServerConfig { module?: string command?: string transport?: string transportPort?: number maxRestartCount?: number disableSnippetCompletion?: boolean disableDynamicRegister?: boolean disabledFeatures?: string[] formatterPriority?: number filetypes: string[] additionalSchemes?: string[] enable?: boolean args?: string[] cwd?: string env?: any // socket port port?: number host?: string detached?: boolean shell?: boolean execArgv?: string[] rootPatterns?: string[] requireRootPattern?: boolean ignoredRootPaths?: string[] initializationOptions?: any progressOnInitialization?: boolean revealOutputChannelOn?: string configSection?: string stdioEncoding?: string runtime?: string } export interface IServiceProvider { // unique service id id: string name: string client?: LanguageClient selector: DocumentSelector // current state state: ServiceStat start(): Promise | void dispose(): void stop(): Promise | void restart(): Promise | void onServiceReady: Event } export interface NotificationItem { id: string method: string } class ServiceManager implements Disposable { private readonly registered: Map = new Map() private disposables: Disposable[] = [] private pendingNotifications: Map = new Map() /** * @deprecated */ public regist /** * @deprecated */ public registLanguageClient constructor() { this.registLanguageClient = this.registerLanguageClient.bind(this) this.regist = this.register.bind(this) } public init(): void { workspace.onDidOpenTextDocument(document => { void this.start(document) }, null, this.disposables) const iterate = (folders: Iterable) => { for (let folder of folders) { this.registerClientsFromFolder(folder) } } workspace.onDidChangeWorkspaceFolders(e => { iterate(e.added) }, null, this.disposables) // `languageserver.${name}` // Global configured languageserver let lspConfig = workspace.initialConfiguration.get<{ key: LanguageServerConfig }>('languageserver', {} as any) this.registerClientsByConfig(lspConfig) iterate(workspace.workspaceFolders) workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('languageserver')) { let lspConfig = workspace.getConfiguration('languageserver', null) this.registerClientsByConfig(lspConfig) } }, null, this.disposables) } private registerClientsFromFolder(workspaceFolder: WorkspaceFolder): void { let uri = URI.parse(workspaceFolder.uri) let lspConfig = workspace.getConfiguration(undefined, uri) let config = lspConfig.inspect('languageserver').workspaceFolderValue this.registerClientsByConfig(config as { [key: string]: LanguageServerConfig }, uri) } public register(service: IServiceProvider): Disposable { let { id } = service if (this.registered.get(id)) return this.registered.set(id, service) this.tryStartService(service) service.onServiceReady(() => { logger.info(`service ${id} started`) }, null, this.disposables) return Disposable.create(() => { if (!this.registered.has(id)) return service.dispose() this.registered.delete(id) }) } public tryStartService(service: IServiceProvider): void { if (!events.ready) { let disposable = events.on('ready', () => { disposable.dispose() if (this.shouldStart(service)) { void service.start() } }) } else if (this.shouldStart(service)) { void service.start() } } public getService(id: string): IServiceProvider { let service = this.registered.get(id) if (!service) service = this.registered.get(`languageserver.${id}`) return service } private shouldStart(service: IServiceProvider): boolean { if (service.state != ServiceStat.Initial) return false let selector = service.selector for (let doc of workspace.documents) { if (workspace.match(selector, doc.textDocument)) { return true } } return false } public async start(document: TextDocument): Promise { let services: IServiceProvider[] = [] for (let service of this.registered.values()) { if (service.state == ServiceStat.Initial && workspace.match(service.selector, document) > 0) { services.push(service) } } // eslint-disable-next-line @typescript-eslint/await-thenable await Promise.allSettled(services.map(service => { return service.start() })) } public stop(id: string): Promise { let service = this.registered.get(id) if (service) return Promise.resolve(service.stop()) } public async toggle(id: string): Promise { let service = this.registered.get(id) if (!service) throw new Error(`Service ${id} not found`) let { state } = service if (state == ServiceStat.Running) { await Promise.resolve(service.stop()) } else if (state == ServiceStat.Initial || state == ServiceStat.StartFailed) { await service.start() } else if (state == ServiceStat.Stopped) { await service.restart() } } public getServiceStats(): ServiceInfo[] { let res: ServiceInfo[] = [] for (let [id, service] of this.registered) { res.push({ id, languageIds: documentSelectorToLanguageIds(service.selector), state: getStateName(service.state) }) } return res } private registerClientsByConfig(lspConfig: { [key: string]: LanguageServerConfig }, folder?: URI): void { for (let key of Object.keys(toObject(lspConfig))) { let config: LanguageServerConfig = lspConfig[key] if (!isValidServerConfig(key, config)) { continue } this.registerLanguageClient(key, config, folder) } } private async getLanguageClient(id: string): Promise { let service = this.getService(id) // wait for extension activate if (!service) await wait(100) service = this.getService(id) if (!service || !service.client) { throw new Error(`Language server ${id} not found`) } return service.client } public async sendNotification(id: string, method: string, params?: any): Promise { let client = await this.getLanguageClient(id) await Promise.resolve(client.sendNotification(method, params)) } public async sendRequest(id: string, method: string, params?: any, token?: CancellationToken): Promise { let client = await this.getLanguageClient(id) token = token ?? CancellationToken.None return await Promise.resolve(client.sendRequest(method, params, token)) } public registerNotification(id: string, method: string): void { let service = this.getService(id) if (service && service.client) { service.client.onNotification(method, async result => { this.sendNotificationVim(id, method, result) }) } let arr = this.pendingNotifications.get(id) ?? [] arr.push({ id, method }) this.pendingNotifications.set(id, arr) } private getRegisteredNotifications(id: string): NotificationItem[] { id = id.startsWith('languageserver') ? id.slice('languageserver.'.length) : id return this.pendingNotifications.get(id) ?? [] } private sendNotificationVim(id: string, method: string, result: any): void { workspace.nvim.call('coc#do_notify', [id, method, result], true) } public registerLanguageClient(client: LanguageClient): Disposable public registerLanguageClient(name: string, config: LanguageServerConfig, folder?: URI): Disposable public registerLanguageClient(name: string | LanguageClient, config?: LanguageServerConfig, folder?: URI): Disposable { let id = typeof name === 'string' ? `languageserver.${name}` : name.id let disposables: Disposable[] = [] let onDidServiceReady = new Emitter() let client: LanguageClient | null = typeof name === 'string' ? null : name if (this.registered.has(id)) return Disposable.create(() => {}) if (client && typeof client.dispose === 'function') disposables.push(client) let created = false let service: IServiceProvider = { id, client, name: typeof name === 'string' ? name : name.name, selector: typeof name === 'string' ? getDocumentSelector(config.filetypes, config.additionalSchemes) : name.clientOptions.documentSelector, state: client && client.state === State.Running ? ServiceStat.Running : ServiceStat.Initial, onServiceReady: onDidServiceReady.event, start: async (): Promise => { if (!created) { if (typeof name == 'string' && !client) { let config: LanguageServerConfig = workspace.getConfiguration(undefined, folder).get(`languageserver.${name}`, {} as any) let opts = getLanguageServerOptions(id, name, config, folder) if (!opts || config.enable === false) return client = new LanguageClient(id, name, opts[1], opts[0]) service.selector = opts[0].documentSelector service.client = client disposables.push(client) } created = true for (let item of this.getRegisteredNotifications(id)) { service.client.onNotification(item.method, async result => { this.sendNotificationVim(item.id, item.method, result) }) } client.onDidChangeState(changeEvent => { let { oldState, newState } = changeEvent service.state = convertState(newState) let oldStr = stateString(oldState) let newStr = stateString(newState) logger.info(`LanguageClient ${client.name} state change: ${oldStr} => ${newStr}`) }, null, disposables) } try { if (!client.needsStart()) { service.state = convertState(client.state) } else { service.state = ServiceStat.Starting logger.debug(`starting service: ${id}`) await client.start() onDidServiceReady.fire(void 0) } } catch (e) { void window.showErrorMessage(`Server ${id} failed to start: ${e}`) logger.error(`Server ${id} failed to start:`, e) service.state = ServiceStat.StartFailed } }, dispose: async () => { onDidServiceReady.dispose() disposeAll(disposables) }, stop: async (): Promise => { if (!client || !client.needsStop()) return await Promise.resolve(client.stop()) }, restart: async (): Promise => { if (client) { service.state = ServiceStat.Starting await client.restart() } else { await service.start() } }, } return this.register(service) } public dispose(): void { disposeAll(this.disposables) for (let service of this.registered.values()) { service.dispose() } this.registered.clear() } } export function documentSelectorToLanguageIds(documentSelector: DocumentSelector): string[] { let res = documentSelector.map(filter => { if (typeof filter == 'string') { return filter } return filter.language }) res = res.filter(s => typeof s == 'string') return Array.from(new Set(res)) } // convert config to options export function getLanguageServerOptions(id: string, name: string, config: Readonly, folder?: URI): [LanguageClientOptions, ServerOptions] { let { command, module, port, args, filetypes } = config args = args || [] if (!filetypes) { void window.showErrorMessage(`Wrong configuration of LS "${name}", filetypes not found`) return null } if (!command && !module && !port) { void window.showErrorMessage(`Wrong configuration of LS "${name}", no command or module specified.`) return null } let serverOptions: ServerOptions if (module) { module = workspace.expand(module) if (!fs.existsSync(module)) { void window.showErrorMessage(`Module file "${module}" not found for LS "${name}"`) return null } serverOptions = { module, runtime: config.runtime ?? process.execPath, args, transport: getTransportKind(config), options: getForkOptions(config) } } else if (command) { serverOptions = { command, args, options: getSpawnOptions(config) } as Executable } else { serverOptions = () => new Promise((resolve, reject) => { let client = new net.Socket() let host = config.host ?? '127.0.0.1' logger.info(`languageserver "${id}" connecting to ${host}:${port}`) client.connect(port, host, () => { resolve({ reader: client, writer: client }) }) client.on('error', e => { reject(new Error(`Connection error for ${id}: ${e.message}`)) }) }) } // compatible let disabledFeatures: string[] = Array.from(config.disabledFeatures || []) for (let key of ['disableWorkspaceFolders', 'disableCompletion', 'disableDiagnostics']) { if (config[key] === true) { logger.warn(`Language server config "${key}" is deprecated, use "disabledFeatures" instead.`) let s = key.slice(7) disabledFeatures.push(s[0].toLowerCase() + s.slice(1)) } } let disableSnippetCompletion = !!config.disableSnippetCompletion let ignoredRootPaths = toArray(config.ignoredRootPaths) let clientOptions: LanguageClientOptions = { workspaceFolder: folder == null ? undefined : { name: path.basename(folder.fsPath), uri: folder.toString() }, rootPatterns: config.rootPatterns, requireRootPattern: config.requireRootPattern, ignoredRootPaths: ignoredRootPaths.map(s => workspace.expand(s)), disableSnippetCompletion, disableDynamicRegister: !!config.disableDynamicRegister, disabledFeatures, formatterPriority: config.formatterPriority, documentSelector: getDocumentSelector(config.filetypes, config.additionalSchemes), revealOutputChannelOn: getRevealOutputChannelOn(config.revealOutputChannelOn), synchronize: { configurationSection: `${id}.settings` }, diagnosticCollectionName: name, outputChannelName: id, stdioEncoding: config.stdioEncoding, progressOnInitialization: config.progressOnInitialization === true, initializationOptions: config.initializationOptions ?? {} } if (config.maxRestartCount) { clientOptions.connectionOptions = { maxRestartCount: config.maxRestartCount } } return [clientOptions, serverOptions] } export function isValidServerConfig(key: string, config: Partial): boolean { let errors: string[] = [] let fields = ['module', 'command', 'transport'] for (let field of fields) { let val = config[field] if (val && typeof val !== 'string') { errors.push(`"${field}" field of languageserver ${key} should be string`) } } if (config.transportPort != null && typeof config.transportPort !== 'number') { errors.push(`"transportPort" field of languageserver ${key} should be number`) } if (!Array.isArray(config.filetypes) || !config.filetypes.every(s => typeof s === 'string')) { errors.push(`"filetypes" field of languageserver ${key} should be array of string`) } if (config.additionalSchemes && (!Array.isArray(config.additionalSchemes) || config.additionalSchemes.some(s => typeof s !== 'string'))) { errors.push(`"additionalSchemes" field of languageserver ${key} should be array of string`) } if (errors.length) { logger.error(`Invalid language server configuration for ${key}`, errors.join('\n')) return false } return true } export function getRevealOutputChannelOn(revealOn: string | undefined): RevealOutputChannelOn { switch (revealOn) { case 'info': return RevealOutputChannelOn.Info case 'warn': return RevealOutputChannelOn.Warn case 'error': return RevealOutputChannelOn.Error case 'never': return RevealOutputChannelOn.Never default: return RevealOutputChannelOn.Never } } export function getDocumentSelector(filetypes: string[] | undefined, additionalSchemes?: string[]): DocumentSelector { let documentSelector: DocumentSelector = [] let schemes = ['file', 'untitled'].concat(additionalSchemes || []) if (!filetypes) return schemes.map(s => ({ scheme: s })) filetypes.forEach(filetype => { documentSelector.push(...schemes.map(scheme => ({ language: filetype, scheme }))) }) return documentSelector } export function getTransportKind(config: LanguageServerConfig): Transport { let { transport, transportPort } = config if (!transport || transport == 'ipc') return TransportKind.ipc if (transport == 'stdio') return TransportKind.stdio if (transport == 'pipe') return TransportKind.pipe return { kind: TransportKind.socket, port: transportPort } } export function getForkOptions(config: LanguageServerConfig): ForkOptions { return { cwd: config.cwd && workspace.expand(config.cwd), execArgv: config.execArgv ?? [], env: config.env ?? undefined } } export function getSpawnOptions(config: LanguageServerConfig): SpawnOptions { return { cwd: config.cwd && workspace.expand(config.cwd), detached: !!config.detached, shell: process.platform === 'win32' || !!config.shell, env: config.env ?? undefined } } export function convertState(state: State): ServiceStat { switch (state) { case State.Running: return ServiceStat.Running case State.Starting: return ServiceStat.Starting case State.Stopped: return ServiceStat.Stopped default: return undefined } } export function stateString(state: State): string { switch (state) { case State.Running: return 'running' case State.Starting: return 'starting' case State.Stopped: return 'stopped' case State.StartFailed: return 'startFailed' default: return 'unknown' } } export function getStateName(state: ServiceStat): string { switch (state) { case ServiceStat.Initial: return 'init' case ServiceStat.Running: return 'running' case ServiceStat.Starting: return 'starting' case ServiceStat.StartFailed: return 'startFailed' case ServiceStat.Stopping: return 'stopping' case ServiceStat.Stopped: return 'stopped' default: return 'unknown' } } export default new ServiceManager() ================================================ FILE: src/snippets/eval.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Range } from 'vscode-languageserver-types' import events from '../events' import { UltiSnippetOption } from '../types' import { isVim } from '../util/constants' import { UltiSnippetContext } from './util' export type EvalKind = 'vim' | 'python' | 'shell' const contexts_var = '__coc_ultisnip_contexts' let context_id = 1 export function generateContextId(bufnr: number): string { return `${bufnr}-${context_id++}` } export function hasPython(snip?: UltiSnippetContext | UltiSnippetOption): boolean { if (!snip) return false if (snip.context) return true if (snip.actions && Object.keys(snip.actions).length > 0) return true return false } export function getResetPythonCode(context: UltiSnippetContext): string[] { const pyCodes: string[] = [] pyCodes.push(`${contexts_var} = ${contexts_var} if '${contexts_var}' in locals() else {}`) pyCodes.push(`context = ${contexts_var}.get('${context.id}', {}).get('context', None)`) pyCodes.push(`match = ${contexts_var}.get('${context.id}', {}).get('match', None)`) return pyCodes } export function getPyBlockCode(snip: UltiSnippetContext): string[] { let { range, line } = snip let pyCodes: string[] = [ 'import re, os, vim, string, random', `path = vim.eval('coc#util#get_fullpath()') or ""`, `fn = os.path.basename(path)`, ] let start = `(${range.start.line},${range.start.character})` let end = `(${range.start.line},${range.end.character})` let indent = line.match(/^\s*/)[0] pyCodes.push(...getResetPythonCode(snip)) pyCodes.push(`snip = SnippetUtil("${escapeString(indent)}", ${start}, ${end}, context)`) return pyCodes } export function getInitialPythonCode(context: UltiSnippetContext): string[] { let pyCodes: string[] = [ 'import re, os, vim, string, random', `path = vim.eval('coc#util#get_fullpath()') or ""`, `fn = os.path.basename(path)`, ] let { range, regex, line, id } = context if (context.context) { pyCodes.push(`snip = ContextSnippet()`) pyCodes.push(`context = ${context.context}`) } else { pyCodes.push(`context = None`) } if (regex && Range.is(range)) { let trigger = line.slice(range.start.character, range.end.character) pyCodes.push(`pattern = re.compile("${escapeString(regex)}")`) pyCodes.push(`match = pattern.search("${escapeString(trigger)}")`) } else { pyCodes.push(`match = None`) } // save 'context and 'match' for synchronize and actions. pyCodes.push(`${contexts_var} = ${contexts_var} if '${contexts_var}' in locals() else {}`) let prefix = id.match(/^\w+-/)[0] // keep context of current buffer only. pyCodes.push(`${contexts_var} = {k: v for k, v in ${contexts_var}.items() if k.startswith('${prefix}')}`) pyCodes.push(`${contexts_var}['${context.id}'] = {'context': context, 'match': match}`) return pyCodes } export async function executePythonCode(nvim: Neovim, codes: string[]) { if (codes.length == 0) return let lines = [...codes] lines.unshift(`__requesting = ${events.requesting ? 'True' : 'False'}`) try { await nvim.command(`pyx ${addPythonTryCatch(lines.join('\n'))}`) } catch (e: any) { let err = new Error(e.message) err.stack = `Error on execute python code:\n${codes.join('\n')}\n` + e.stack throw err } } export function getVariablesCode(values: { [index: number]: string }): string { let keys = Object.keys(values) if (keys.length == 0) return `t = ()` let maxIndex = Math.max.apply(null, keys.map(v => Number(v))) let vals = (new Array(maxIndex)).fill('""') for (let [idx, val] of Object.entries(values)) { vals[idx] = `"${escapeString(val)}"` } return `t = (${vals.join(',')},)` } /** * vim8 doesn't throw any python error with :py command * we have to use g:errmsg since v:errmsg can't be changed in python script. */ export function addPythonTryCatch(code: string, force = false): string { if (!isVim && force === false) return code let lines = [ 'import traceback, vim', `vim.vars['errmsg'] = ''`, 'try:', ] lines.push(...code.split('\n').map(line => ' ' + line)) lines.push('except Exception as e:') lines.push(` vim.vars['errmsg'] = traceback.format_exc()`) return lines.join('\n') } export function escapeString(input: string): string { return input .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\t/g, '\\t') .replace(/\n/g, '\\n') } ================================================ FILE: src/snippets/manager.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { InsertTextMode, Position, Range, TextEdit } from 'vscode-languageserver-types' import commands from '../commands' import events from '../events' import BufferSync from '../model/bufferSync' import { StatusBarItem } from '../model/status' import { UltiSnippetOption } from '../types' import { defaultValue, disposeAll } from '../util' import { deepClone } from '../util/object' import { emptyRange, toValidRange } from '../util/position' import { Disposable } from '../util/protocol' import window from '../window' import workspace from '../workspace' import { executePythonCode, generateContextId, getInitialPythonCode, hasPython } from './eval' import { SnippetConfig, SnippetEdit, SnippetSession } from './session' import { SnippetString } from './string' import { getAction, normalizeSnippetString, shouldFormat, SnippetFormatOptions, toSnippetString, UltiSnippetContext } from './util' export class SnippetManager { private disposables: Disposable[] = [] private _statusItem: StatusBarItem private bufferSync: BufferSync private config: SnippetConfig public init() { this.synchronizeConfig() workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('snippet') || e.affectsConfiguration('suggest')) { this.synchronizeConfig() } }, null, this.disposables) events.on(['InsertCharPre', 'Enter'], () => { let session = this.session if (session) session.cancel() }, null, this.disposables) events.on('CompleteDone', async (_item, _line, bufnr) => { let session = this.bufferSync.getItem(bufnr) if (session) await session.onCompleteDone() }, null, this.disposables) events.on('CompleteStart', async opt => { let session = this.bufferSync.getItem(opt.bufnr) if (session) session.cancel(true) }, null, this.disposables) events.on('InsertEnter', async bufnr => { let session = this.bufferSync.getItem(bufnr) if (session) await session.checkPosition() }, null, this.disposables) this.bufferSync = workspace.registerBufferSync(doc => { let session = new SnippetSession(this.nvim, doc, this.config) session.onActiveChange(isActive => { if (events.bufnr !== session.bufnr) return this.statusItem[isActive ? 'show' : 'hide']() }) return session }) this.disposables.push(this.bufferSync) window.onDidChangeActiveTextEditor(async e => { let session = this.bufferSync.getItem(e.bufnr) if (session && session.isActive) { this.statusItem.show() if (!session.selected) { await session.selectCurrentPlaceholder() } } else { this.statusItem.hide() } }, null, this.disposables) commands.register({ id: 'editor.action.insertSnippet', execute: async (edit: TextEdit, ultisnip?: UltiSnippetOption | true) => { const opts = ultisnip === true ? {} : ultisnip return await this.insertSnippet(edit.newText, true, edit.range, InsertTextMode.adjustIndentation, opts ? opts : undefined) } }, true) commands.register({ id: 'editor.action.insertBufferSnippets', execute: async (bufnr: number, edits: SnippetEdit[], select: boolean) => { return await this.insertBufferSnippets(bufnr, edits, select) } }, true) } private get nvim(): Neovim { return workspace.nvim } private get statusItem(): StatusBarItem { if (this._statusItem) return this._statusItem const snippetConfig = workspace.initialConfiguration.get('snippet') as any const statusItem = this._statusItem = window.createStatusBarItem(0) statusItem.text = defaultValue(snippetConfig.statusText, '') return this._statusItem } private synchronizeConfig(): void { const snippetConfig = workspace.getConfiguration('snippet', null) const suggest = workspace.getConfiguration('suggest', null) let obj = { highlight: defaultValue(snippetConfig.inspect('highlight').globalValue, false) as boolean, nextOnDelete: defaultValue(snippetConfig.inspect('nextPlaceholderOnDelete').globalValue, false) as boolean, preferComplete: suggest.get('preferCompleteThanJumpPlaceholder', false) } if (this.config) { Object.assign(this.config, obj) } else { this.config = obj } } private async toRange(range: Range | undefined): Promise { if (range) return toValidRange(range) let pos = await window.getCursorPosition() return Range.create(pos, pos) } public async insertBufferSnippets(bufnr: number, edits: SnippetEdit[], select = false): Promise { let document = workspace.getAttachedDocument(bufnr) const session = this.bufferSync.getItem(bufnr) session.cancel(true) let snippetEdits: SnippetEdit[] = [] for (const edit of edits) { let currentLine = document.getline(edit.range.start.line) let inserted = await this.normalizeInsertText(bufnr, toSnippetString(edit.snippet), currentLine, InsertTextMode.asIs) snippetEdits.push({ range: edit.range, snippet: inserted }) } await session.synchronize() let isActive = await session.insertSnippetEdits(snippetEdits) if (isActive && select && workspace.bufnr === bufnr) { await session.selectCurrentPlaceholder() } return isActive } /** * Insert snippet to specific buffer, ultisnips not supported, and the placeholder is not selected */ public async insertBufferSnippet(bufnr: number, snippet: string | SnippetString, range: Range, insertTextMode?: InsertTextMode): Promise { let document = workspace.getAttachedDocument(bufnr) const session = this.bufferSync.getItem(bufnr) session.cancel(true) range = toValidRange(range) const line = document.getline(range.start.line) const snippetStr = toSnippetString(snippet) const inserted = await this.normalizeInsertText(document.bufnr, snippetStr, line, insertTextMode) await session.synchronize() return await session.start(inserted, range, false) } /** * Insert snippet at current cursor position */ public async insertSnippet(snippet: string | SnippetString, select = true, range?: Range, insertTextMode?: InsertTextMode, ultisnip?: UltiSnippetOption): Promise { let { nvim } = workspace let document = workspace.getAttachedDocument(workspace.bufnr) const session = this.bufferSync.getItem(document.bufnr) let context: UltiSnippetContext session.cancel(true) range = await this.toRange(range) const currentLine = document.getline(range.start.line) const snippetStr = toSnippetString(snippet) const inserted = await this.normalizeInsertText(document.bufnr, snippetStr, currentLine, insertTextMode, ultisnip) if (ultisnip != null) { const usePy = hasPython(ultisnip) || inserted.includes('`!p') const bufnr = document.bufnr context = Object.assign({ range: deepClone(range), line: currentLine }, ultisnip, { id: generateContextId(bufnr) }) if (usePy) { if (session.placeholder) { let { start, end } = session.placeholder.range let last = { current_text: session.placeholder.value, start: { line: start.line, col: start.character }, end: { line: end.line, col: end.character } } this.nvim.setVar('coc_last_placeholder', last, true) } else { this.nvim.call('coc#compat#del_var', ['coc_last_placeholder'], true) } const codes = getInitialPythonCode(context) let preExpand = getAction(ultisnip, 'preExpand') if (preExpand) { nvim.call('coc#cursor#move_to', [range.end.line, range.end.character], true) await executePythonCode(nvim, codes.concat(['snip = coc_ultisnips_dict["PreExpandContext"]()', preExpand])) const [valid, pos] = await nvim.call('pyxeval', 'snip.getResult()') as [boolean, [number, number]] // need remove the trigger if (valid) { let count = range.end.character - range.start.character range = Range.create(pos[0], Math.max(0, pos[1] - count), pos[0], pos[1]) } else { // trigger removed already range = Range.create(pos[0], pos[1], pos[0], pos[1]) } } else { await executePythonCode(nvim, codes) } } } // same behavior as Ultisnips const noMove = ultisnip == null && !session.isActive if (!noMove) { const { start } = range nvim.call('coc#cursor#move_to', [start.line, start.character], true) // range could outside snippet range when session synchronize is canceled if (!emptyRange(range)) { await document.applyEdits([TextEdit.del(range)]) } if (session.isActive) { await session.synchronize() // the cursor position could be changed on session synchronize. let pos = await window.getCursorPosition() range = Range.create(pos, pos) } else { range.end = Position.create(start.line, start.character) } } await session.start(inserted, range, select, context) return session.isActive } public async selectCurrentPlaceholder(triggerAutocmd = true): Promise { let { session } = this if (session) return await session.selectCurrentPlaceholder(triggerAutocmd) } public async nextPlaceholder(): Promise { let { session } = this if (session) { await session.nextPlaceholder() } else { this.nvim.call('coc#snippet#disable', [], true) } return '' } public async previousPlaceholder(): Promise { let { session } = this if (session) { await session.previousPlaceholder() } else { this.nvim.call('coc#snippet#disable', [], true) } return '' } public cancel(): void { let session = this.bufferSync.getItem(workspace.bufnr) if (session) return session.deactivate() this.nvim.call('coc#snippet#disable', [], true) this.statusItem.hide() } public get session(): SnippetSession | undefined { return this.bufferSync.getItem(workspace.bufnr) } /** * exported method */ public getSession(bufnr: number): SnippetSession | undefined { let session = this.bufferSync.getItem(bufnr) return session && session.isActive ? session : undefined } public isActivated(bufnr: number): boolean { let session = this.bufferSync.getItem(bufnr) return session && session.isActive } public jumpable(): boolean { let { session } = this if (!session) return false return session.placeholder != null && session.placeholder.index != 0 } /** * Exposed for snippet preview */ public async resolveSnippet(snippetString: string, ultisnip?: UltiSnippetOption): Promise { let session = this.bufferSync.getItem(workspace.bufnr) if (!session) return return await session.resolveSnippet(this.nvim, snippetString, ultisnip) } public async normalizeInsertText(bufnr: number, snippetString: string, currentLine: string, insertTextMode: InsertTextMode, ultisnip?: Partial): Promise { let inserted = '' if (insertTextMode === InsertTextMode.asIs || !shouldFormat(snippetString)) { inserted = snippetString } else { const currentIndent = currentLine.match(/^\s*/)[0] let formatOptions = await workspace.getFormatOptions(bufnr) as SnippetFormatOptions let opts: Partial = ultisnip ?? {} // trim when option not exists formatOptions.trimTrailingWhitespace = opts.trimTrailingWhitespace !== false if (opts.noExpand) formatOptions.noExpand = true inserted = normalizeSnippetString(snippetString, currentIndent, formatOptions) } return inserted } public dispose(): void { this.cancel() disposeAll(this.disposables) } } export default new SnippetManager() ================================================ FILE: src/snippets/parser.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { exec, ExecOptions } from 'child_process' import { CancellationToken } from 'vscode-languageserver-protocol' import { createLogger } from '../logger' import { groupBy } from '../util/array' import { runSequence } from '../util/async' import { CharCode } from '../util/charCode' import { onUnexpectedError } from '../util/errors' import { promisify, unidecode } from '../util/node' import { iterateCharacter, toText } from '../util/string' import { escapeString, EvalKind, executePythonCode, getVariablesCode } from './eval' import { convertRegex, UltiSnippetContext } from './util' const logger = createLogger('snippets-parser') const ULTISNIP_VARIABLES = ['VISUAL', 'YANK', 'UUID'] let id = 0 let snippet_id = 0 const knownRegexOptions = ['d', 'g', 'i', 'm', 's', 'u', 'y'] const ultisnipSpecialEscape = ['u', 'l', 'U', 'L', 'E', 'n', 't'] export const enum TokenType { Dollar, Colon, Comma, CurlyOpen, CurlyClose, Backslash, Forwardslash, Pipe, Int, VariableName, Format, Plus, Dash, QuestionMark, EOF, OpenParen, CloseParen, BackTick, ExclamationMark, } export interface Token { type: TokenType pos: number len: number } export class Scanner { private static _table: { [ch: number]: TokenType } = { [CharCode.DollarSign]: TokenType.Dollar, [CharCode.Colon]: TokenType.Colon, [CharCode.Comma]: TokenType.Comma, [CharCode.OpenCurlyBrace]: TokenType.CurlyOpen, [CharCode.CloseCurlyBrace]: TokenType.CurlyClose, [CharCode.Backslash]: TokenType.Backslash, [CharCode.Slash]: TokenType.Forwardslash, [CharCode.Pipe]: TokenType.Pipe, [CharCode.Plus]: TokenType.Plus, [CharCode.Dash]: TokenType.Dash, [CharCode.QuestionMark]: TokenType.QuestionMark, [CharCode.OpenParen]: TokenType.OpenParen, [CharCode.CloseParen]: TokenType.CloseParen, [CharCode.BackTick]: TokenType.BackTick, [CharCode.ExclamationMark]: TokenType.ExclamationMark, } public static isDigitCharacter(ch: number): boolean { return ch >= CharCode.Digit0 && ch <= CharCode.Digit9 } public static isVariableCharacter(ch: number): boolean { return ch === CharCode.Underline || (ch >= CharCode.a && ch <= CharCode.z) || (ch >= CharCode.A && ch <= CharCode.Z) } public value: string public pos: number constructor() { this.text('') } public text(value: string): void { this.value = value this.pos = 0 } public tokenText(token: Token): string { return this.value.substr(token.pos, token.len) } public isEnd(): boolean { return this.pos >= this.value.length } public next(): Token { if (this.pos >= this.value.length) { return { type: TokenType.EOF, pos: this.pos, len: 0 } } let pos = this.pos let len = 0 let ch = this.value.charCodeAt(pos) let type: TokenType // static types type = Scanner._table[ch] if (typeof type === 'number') { this.pos += 1 return { type, pos, len: 1 } } // number if (Scanner.isDigitCharacter(ch)) { type = TokenType.Int do { len += 1 ch = this.value.charCodeAt(pos + len) } while (Scanner.isDigitCharacter(ch)) this.pos += len return { type, pos, len } } // variable name if (Scanner.isVariableCharacter(ch)) { type = TokenType.VariableName do { ch = this.value.charCodeAt(pos + (++len)) } while (Scanner.isVariableCharacter(ch) || Scanner.isDigitCharacter(ch)) this.pos += len return { type, pos, len } } // format type = TokenType.Format do { len += 1 ch = this.value.charCodeAt(pos + len) } while ( !isNaN(ch) && typeof Scanner._table[ch] === 'undefined' // not static token && !Scanner.isDigitCharacter(ch) // not number && !Scanner.isVariableCharacter(ch) // not variable ) this.pos += len return { type, pos, len } } } export abstract class Marker { public parent: Marker protected _children: Marker[] = [] public appendChild(child: Marker): this { if (child instanceof Text && this._children[this._children.length - 1] instanceof Text) { // this and previous child are text -> merge them (this._children[this._children.length - 1] as Text).value += child.value } else { // normal adoption of child child.parent = this this._children.push(child) } return this } public setOnlyChild(child: Marker): void { child.parent = this this._children = [child] } public replaceChildren(children: Marker[]): void { for (const child of children) { child.parent = this } this._children = children } public replaceWith(newMarker: Marker): boolean { if (!this.parent) return false let p = this.parent let idx = p.children.indexOf(this) if (idx == -1) return false newMarker.parent = p p.children.splice(idx, 1, newMarker) return true } public insertBefore(text: string): void { if (!this.parent) return let p = this.parent let idx = p.children.indexOf(this) if (idx == -1) return let prev = p.children[idx - 1] if (prev instanceof Text) { let v = prev.value prev.replaceWith(new Text(v + text)) } else { let marker = new Text(text) marker.parent = p p.children.splice(idx, 0, marker) } } public get children(): Marker[] { return this._children } public get snippet(): TextmateSnippet | undefined { // eslint-disable-next-line @typescript-eslint/no-this-alias let candidate: Marker = this while (true) { if (!candidate) { return undefined } if (candidate instanceof TextmateSnippet) { return candidate } candidate = candidate.parent } } public toString(): string { return this.children.reduce((prev, cur) => prev + cur.toString(), '') } public abstract toTextmateString(): string public len(): number { return 0 } public abstract clone(): Marker } export class Text extends Marker { public static escape(value: string): string { return value.replace(/\$|}|\\/g, '\\$&') } constructor(public value: string) { super() } public toString(): string { return this.value } public toTextmateString(): string { return Text.escape(this.value) } public len(): number { return this.value.length } public clone(): Text { return new Text(this.value) } } export class CodeBlock extends Marker { private _value = '' private _related: number[] = [] constructor(public code: string, public readonly kind: EvalKind, value?: string, related?: number[]) { super() if (Array.isArray(related)) { this._related = related } else if (kind === 'python') { this._related = CodeBlock.parseRelated(code) } if (typeof value === 'string') this._value = value } public static parseRelated(code: string): number[] { let list: number[] = [] let arr let re = /\bt\[(\d+)\]/g while (true) { arr = re.exec(code) if (arr == null) break let n = parseInt(arr[1], 10) if (!list.includes(n)) list.push(n) } return list } public get related(): number[] { return this._related } public get index(): number | undefined { if (this.parent instanceof Placeholder) { return this.parent.index } return undefined } public async resolve(nvim: Neovim, token?: CancellationToken): Promise { if (!this.code.length) return if (token?.isCancellationRequested) return let res: string if (this.kind == 'python') { res = await this.evalPython(nvim, token) } else if (this.kind == 'vim') { res = await this.evalVim(nvim) } else if (this.kind == 'shell') { res = await this.evalShell() } if (token?.isCancellationRequested) return if (res != null) this._value = res } public async evalShell(): Promise { let opts: ExecOptions = { windowsHide: true } Object.assign(opts, { shell: process.env.SHELL }) let res = await promisify(exec)(this.code, opts) return res.stdout.replace(/\s*$/, '') } public async evalVim(nvim: Neovim): Promise { let res = await nvim.eval(this.code) return res == null ? '' : res.toString() } public async evalPython(nvim: Neovim, token?: CancellationToken): Promise { let curr = toText(this._value) let lines = [`snip._reset("${escapeString(curr)}")`] lines.push(...this.code.split(/\r?\n/).map(line => line.replace(/\t/g, ' '))) await executePythonCode(nvim, lines) if (token?.isCancellationRequested) return return await nvim.call(`pyxeval`, 'str(snip.rv)') as string } public len(): number { return this._value.length } public toString(): string { return this._value } public get value(): string { return this._value } public toTextmateString(): string { let t = '' if (this.kind == 'python') { t = '!p ' } else if (this.kind == 'shell') { t = '' } else if (this.kind == 'vim') { t = '!v ' } return '`' + t + (this.code) + '`' } public clone(): CodeBlock { return new CodeBlock(this.code, this.kind, this.value, this._related.slice()) } } abstract class TransformableMarker extends Marker { public transform: Transform } export class Placeholder extends TransformableMarker { public primary = false public id: number constructor(public index: number) { super() } public get isFinalTabstop(): boolean { return this.index === 0 } public get choice(): Choice | undefined { return this._children.length === 1 && this._children[0] instanceof Choice ? this._children[0] as Choice : undefined } public toTextmateString(): string { let transformString = '' if (this.transform) { transformString = this.transform.toTextmateString() } if (this.children.length === 0 && !this.transform) { return `$${this.index}` } else if (this.children.length === 0 || (this.children.length == 1 && this.children[0].toTextmateString() == '')) { return `\${${this.index}${transformString}}` } else if (this.choice) { return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}` } else { return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}` } } public clone(): Placeholder { let ret = new Placeholder(this.index) if (this.transform) { ret.transform = this.transform.clone() } ret.id = this.id ret.primary = this.primary ret._children = this.children.map(child => { let m = child.clone() m.parent = ret return m }) return ret } public checkParentPlaceHolders(): void { let idx = this.index let p = this.parent while (p != null && !(p instanceof TextmateSnippet)) { if (p instanceof Placeholder && p.index == idx) { throw new Error(`Parent placeholder has same index: ${idx}`) } p = p.parent } } } export class Choice extends Marker { private _index constructor(index = 0) { super() this._index = index } public readonly options: Text[] = [] public appendChild(marker: Marker): this { if (marker instanceof Text) { marker.parent = this this.options.push(marker) } return this } public toString(): string { return this.options[this._index].value } public toTextmateString(): string { return this.options .map(option => option.value.replace(/\||,/g, '\\$&')) .join(',') } public len(): number { return this.options[this._index].len() } public clone(): Choice { let ret = new Choice(this._index) for (let opt of this.options) { ret.appendChild(opt) } return ret } } export class Transform extends Marker { public regexp: RegExp public ascii = false public ultisnip = false public resolve(value: string): string { let didMatch = false let ret = value.replace(this.regexp, (...args) => { didMatch = true return this._replace(args.slice(0, -2)) }) // when the regex didn't match and when the transform has // else branches, then run those if (!didMatch && this._children.some(child => child instanceof FormatString && Boolean(child.elseValue))) { ret = this._replace([]) } return ret } private _replace(groups: string[]): string { let ret = '' let backslashIndexes: number[] = [] for (const marker of this._children) { let val = '' let len = ret.length if (marker instanceof FormatString) { val = marker.resolve(groups[marker.index] ?? '') if (this.ultisnip && val.indexOf('\\') !== -1) { for (let idx of iterateCharacter(val, '\\')) { backslashIndexes.push(len + idx) } } } else if (marker instanceof ConditionString) { val = marker.resolve(groups[marker.index]) if (this.ultisnip) { val = val.replace(/(? { return toText(groups[Number(args[1])]) }) } } else { val = marker.toString() } ret += val } if (this.ascii) ret = unidecode(ret) return this.ultisnip ? transformEscapes(ret, backslashIndexes) : ret } public toString(): string { return '' } public toTextmateString(): string { let format = this.children.map(c => c.toTextmateString()).join('') if (this.ultisnip) { // avoid bad escape of Text for ultisnip format format = format.replace(/\\\\(\w)/g, (match, ch) => { if (ultisnipSpecialEscape.includes(ch)) { return '\\' + ch } return match }) } return `/${this.regexp.source}/${format}/${(this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')}` } public clone(): Transform { let ret = new Transform() ret.regexp = new RegExp(this.regexp.source, '' + (this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')) ret._children = this.children.map(child => { let m = child.clone() m.parent = ret return m }) return ret } } export class ConditionString extends Marker { constructor( public readonly index: number, public readonly ifValue: string, public readonly elseValue: string, ) { super() } public resolve(value: string): string { if (value) return this.ifValue return this.elseValue } public toTextmateString(): string { return '(?' + this.index + ':' + this.ifValue + (this.elseValue ? ':' + this.elseValue : '') + ')' } public clone(): ConditionString { return new ConditionString(this.index, this.ifValue, this.elseValue) } } // TODO ultisnip only, not used yet export class ConditionMarker extends Marker { constructor( public readonly index: number, protected ifMarkers: Marker[], protected elseMarkers: Marker[], ) { super() } public resolve(value: string, groups: string[]): string { let fn = (p: string, c: Marker): string => { return p + (c instanceof FormatString ? c.resolve(groups[c.index]) : c.toString()) } if (value) return this.ifMarkers.reduce(fn, '') return this.elseMarkers.reduce(fn, '') } public addIfMarker(marker: Marker) { this.ifMarkers.push(marker) } public addElseMarker(marker: Marker) { this.elseMarkers.push(marker) } public toTextmateString(): string { let ifValue = this.ifMarkers.reduce((p, c) => p + c.toTextmateString(), '') let elseValue = this.elseMarkers.reduce((p, c) => p + c.toTextmateString(), '') return '(?' + this.index + ':' + ifValue + (elseValue.length > 0 ? ':' + elseValue : '') + ')' } public clone(): ConditionMarker { return new ConditionMarker(this.index, this.ifMarkers.map(m => m.clone()), this.elseMarkers.map(m => m.clone())) } } export class FormatString extends Marker { constructor( public readonly index: number, public readonly shorthandName?: string, public readonly ifValue?: string, public readonly elseValue?: string, ) { super() } public resolve(value: string): string { if (this.shorthandName === 'upcase') { return !value ? '' : value.toLocaleUpperCase() } else if (this.shorthandName === 'downcase') { return !value ? '' : value.toLocaleLowerCase() } else if (this.shorthandName === 'capitalize') { return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1)) } else if (this.shorthandName === 'pascalcase') { return !value ? '' : this._toPascalCase(value) } else if (Boolean(value) && typeof this.ifValue === 'string') { return this.ifValue } else if (!value && typeof this.elseValue === 'string') { return this.elseValue } else { return value || '' } } private _toPascalCase(value: string): string { const match = value.match(/[a-z]+/gi) if (!match) { return value } return match.map(word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()) .join('') } public toTextmateString(): string { let value = '${' value += this.index if (this.shorthandName) { value += `:/${this.shorthandName}` } else if (this.ifValue && this.elseValue) { value += `:?${this.ifValue}:${this.elseValue}` } else if (this.ifValue) { value += `:+${this.ifValue}` } else if (this.elseValue) { value += `:-${this.elseValue}` } value += '}' return value } public clone(): FormatString { let ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue) return ret } } export class Variable extends TransformableMarker { private _resolved: boolean constructor(public name: string, resolved = false) { super() this._resolved = resolved } public get resolved(): boolean { return this._resolved } public async resolve(resolver: VariableResolver): Promise { let value = await resolver.resolve(this) this._resolved = true if (value && value.includes('\n')) { // get indent from previous texts let indent = '' this.snippet.walk(m => { if (m == this) { return false } if (m instanceof Text) { let lines = m.toString().split(/\r?\n/) indent = lines[lines.length - 1].match(/^\s*/)[0] } return true }, true) let lines = value.split('\n') let indents = lines.filter(s => s.length > 0).map(s => s.match(/^\s*/)[0]) let minIndent = indents.reduce((p, c) => p < c.length ? p : c.length, 0) let newLines = lines.map((s, i) => i == 0 || s.length == 0 || !s.startsWith(' '.repeat(minIndent)) ? s : indent + s.slice(minIndent)) value = newLines.join('\n') } if (typeof value !== 'string') return false if (this.transform) { value = this.transform.resolve(toText(value)) } this._children = [new Text(value.toString())] return true } public toTextmateString(): string { let transformString = '' if (this.transform) { transformString = this.transform.toTextmateString() } if (this.children.length === 0) { return `\${${this.name}${transformString}}` } else { return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}` } } public clone(): Variable { const ret = new Variable(this.name, this.resolved) if (this.transform) { ret.transform = this.transform.clone() } ret._children = this.children.map(child => { let m = child.clone() m.parent = ret return m }) return ret } } export interface VariableResolver { resolve(variable: Variable): Promise } export interface PlaceholderInfo { placeholders: Placeholder[] pyBlocks: CodeBlock[] otherBlocks: CodeBlock[] } function walk(marker: Marker[], visitor: (marker: Marker) => boolean, ignoreChild = false): void { const stack = [...marker] while (stack.length > 0) { const marker = stack.shift() if (ignoreChild && marker instanceof TextmateSnippet) continue const recurse = visitor(marker) if (!recurse) { break } stack.unshift(...marker.children) } } export class TextmateSnippet extends Marker { public readonly ultisnip: boolean public readonly id: number public readonly related: { codes?: string[], context?: UltiSnippetContext } = {} constructor(ultisnip?: boolean, id?: number) { super() this.ultisnip = ultisnip === true this.id = id ?? snippet_id++ } public get hasPythonBlock(): boolean { if (!this.ultisnip) return false return this.pyBlocks.length > 0 } public get hasCodeBlock(): boolean { if (!this.ultisnip) return false let { pyBlocks, otherBlocks } = this return pyBlocks.length > 0 || otherBlocks.length > 0 } /** * Values for each placeholder index */ public get values(): { [index: number]: string } { let values: { [index: number]: string } = {} let maxIndexNumber = 0 this.placeholders.forEach(c => { if (!Number.isInteger(c.index)) return maxIndexNumber = Math.max(c.index, maxIndexNumber) if (c.transform != null) return if (c.primary || values[c.index] === undefined) values[c.index] = c.toString() }) for (let i = 0; i <= maxIndexNumber; i++) { if (values[i] === undefined) values[i] = '' } return values } public get orderedPyIndexBlocks(): CodeBlock[] { let res: CodeBlock[] = [] let filtered = this.pyBlocks.filter(o => typeof o.index === 'number') if (filtered.length === 0) return res let allIndexes = filtered.map(o => o.index) let usedIndexes: number[] = [] const checkBlock = (b: CodeBlock): boolean => { let { related } = b if (related.length == 0 || related.every(idx => !allIndexes.includes(idx) || usedIndexes.includes(idx))) { usedIndexes.push(b.index) res.push(b) return true } return false } while (filtered.length > 0) { let c = false for (let b of filtered) { if (checkBlock(b)) { c = true } } if (!c) { // recursive dependencies detected break } filtered = filtered.filter(o => !usedIndexes.includes(o.index)) } return res } public async evalCodeBlocks(nvim: Neovim, pyCodes: string[]): Promise { const { pyBlocks, otherBlocks } = this.placeholderInfo // update none python blocks await Promise.all(otherBlocks.map(block => { let pre = block.value return block.resolve(nvim).then(() => { if (block.parent instanceof Placeholder && pre !== block.value) { // update placeholder with same index this.onPlaceholderUpdate(block.parent) } }) })) if (pyCodes.length === 0) return // update normal python block with related. let relatedBlocks = pyBlocks.filter(o => o.index === undefined && o.related.length > 0) // run all python code by sequence const variableCode = getVariablesCode(this.values) await executePythonCode(nvim, [...pyCodes, variableCode]) for (let block of pyBlocks) { let pre = block.value if (relatedBlocks.includes(block)) continue await block.resolve(nvim) if (pre === block.value) continue if (block.parent instanceof Placeholder) { // update placeholder with same index this.onPlaceholderUpdate(block.parent) await executePythonCode(nvim, [getVariablesCode(this.values)]) } } for (let block of this.orderedPyIndexBlocks) { await this.updatePyIndexBlock(nvim, block) } for (let block of relatedBlocks) { await block.resolve(nvim) } } /** * Update python blocks after user change Placeholder with index */ public async updatePythonCodes(nvim: Neovim, marker: Placeholder, codes: string[], token: CancellationToken): Promise { let index = marker.index // update related placeholders let blocks = this.getDependentPyIndexBlocks(index) await runSequence([async () => { await executePythonCode(nvim, [...codes, getVariablesCode(this.values)]) }, async () => { for (let block of blocks) { await this.updatePyIndexBlock(nvim, block, token) } }, async () => { // update normal pyBlocks. let filtered = this.pyBlocks.filter(o => o.index === undefined && o.related.length > 0) for (let block of filtered) { await block.resolve(nvim, token) } }], token) } private getDependentPyIndexBlocks(index: number): CodeBlock[] { const res: CodeBlock[] = [] const taken: number[] = [] let filtered = this.pyBlocks.filter(o => typeof o.index === 'number') const search = (idx: number) => { let blocks = filtered.filter(o => !taken.includes(o.index) && o.related.includes(idx)) if (blocks.length > 0) { res.push(...blocks) blocks.forEach(b => { search(b.index) }) } } search(index) return res } /** * Update single index block */ private async updatePyIndexBlock(nvim: Neovim, block: CodeBlock, token?: CancellationToken): Promise { let pre = block.value await block.resolve(nvim, token) if (pre === block.value || token?.isCancellationRequested) return if (block.parent instanceof Placeholder) { this.onPlaceholderUpdate(block.parent) } await executePythonCode(nvim, [getVariablesCode(this.values)]) } public get placeholderInfo(): PlaceholderInfo { const pyBlocks: CodeBlock[] = [] const otherBlocks: CodeBlock[] = [] // fill in placeholders let placeholders: Placeholder[] = [] this.walk(candidate => { if (candidate instanceof Placeholder) { placeholders.push(candidate) } else if (candidate instanceof CodeBlock) { if (candidate.kind === 'python') { pyBlocks.push(candidate) } else { otherBlocks.push(candidate) } } return true }, true) return { placeholders, pyBlocks, otherBlocks } } public get variables(): Variable[] { const variables = [] this.walk(candidate => { if (candidate instanceof Variable) { variables.push(candidate) } return true }, true) return variables } public get placeholders(): Placeholder[] { let placeholders: Placeholder[] = [] this.walk(candidate => { if (candidate instanceof Placeholder) { placeholders.push(candidate) } return true }, true) return placeholders } public get pyBlocks(): CodeBlock[] { return this.placeholderInfo.pyBlocks } public get otherBlocks(): CodeBlock[] { return this.placeholderInfo.otherBlocks } public get first(): Placeholder { let { placeholders } = this let [normals, finals] = groupBy(placeholders.filter(p => !p.transform), v => v.index !== 0) if (normals.length) { let minIndex = Math.min.apply(null, normals.map(o => o.index)) let arr = normals.filter(v => v.index == minIndex) return arr.find(p => p.primary) ?? arr[0] } return finals.find(o => o.primary) ?? finals[0] } public async update(nvim: Neovim, marker: Placeholder, token: CancellationToken): Promise { this.onPlaceholderUpdate(marker) let codes = this.related.codes ?? [] if (codes.length === 0 || !this.hasPythonBlock) return await this.updatePythonCodes(nvim, marker, codes, token) } /** * Reflact changes for related markers. */ public onPlaceholderUpdate(marker: Placeholder): void { let val = marker.toString() let markers = this.placeholders.filter(o => o.index == marker.index) for (let p of markers) { p.checkParentPlaceHolders() if (p === marker) continue let newText = p.transform ? p.transform.resolve(val) : val p.setOnlyChild(new Text(toText(newText))) } this.synchronizeParents(markers) } public synchronizeParents(markers: Marker[]): void { let parents: Set = new Set() markers.forEach(m => { let p = m.parent if (p instanceof Placeholder) parents.add(p) }) for (let p of parents) { this.onPlaceholderUpdate(p) } } public offset(marker: Marker): number { let pos = 0 let found = false this.walk(candidate => { if (candidate === marker) { found = true return false } pos += candidate.len() return true }, true) if (!found) { return -1 } return pos } public fullLen(marker: Marker): number { let ret = 0 walk([marker], marker => { ret += marker.len() return true }) return ret } public getTextBefore(marker: Marker, parent: Placeholder): string { let res = '' const calc = (m: Marker): void => { let p = m.parent if (!p) return let s = '' for (let b of p.children) { if (b === m) break s = s + b.toString() } res = s + res if (p == parent) return calc(p) } calc(marker) return res } public enclosingPlaceholders(placeholder: Placeholder | Variable): Placeholder[] { let ret: Placeholder[] = [] let { parent } = placeholder while (parent) { if (parent instanceof Placeholder) { ret.push(parent) } parent = parent.parent } return ret } public async resolveVariables(resolver: VariableResolver): Promise { let variables = this.variables if (variables.length === 0) return let failed: Variable[] = [] let succeed: Variable[] = [] let promises: Promise[] = [] const changedParents: Set = new Set() for (let item of variables) { promises.push(item.resolve(resolver).then(res => { changedParents.add(item.parent) let arr = res ? succeed : failed arr.push(item) }, onUnexpectedError)) } await Promise.allSettled(promises) // convert resolved variables to text for (const variable of succeed) { let text = new Text(variable.toString()) variable.replaceWith(text) } if (failed.length > 0) { // convert to placeholders let indexMap: Map = new Map() const primarySet: Set = new Set() // create index for variables let max = this.getMaxPlaceholderIndex() for (let i = 0; i < failed.length; i++) { const v = failed[i] let idx = indexMap.get(v.name) if (idx == null) { idx = ++max indexMap.set(v.name, idx) } let p = new Placeholder(idx) p.transform = v.transform if (!p.transform && !primarySet.has(idx)) { primarySet.add(idx) p.primary = true } let newText = p.transform ? p.transform.resolve(v.name) : v.name p.setOnlyChild(new Text(toText(newText))) v.replaceWith(p) } } changedParents.forEach(marker => { mergeTexts(marker) if (marker instanceof Placeholder) this.onPlaceholderUpdate(marker) }) } public getMaxPlaceholderIndex(): number { let res = 0 this.walk(candidate => { if (candidate instanceof Placeholder) { res = Math.max(res, candidate.index) } return true }, true) return res } public replace(marker: Marker, children: Marker[]): void { marker.replaceChildren(children) if (marker instanceof Placeholder) { this.onPlaceholderUpdate(marker) } } public toTextmateString(): string { return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), '') } public clone(): TextmateSnippet { let ret = new TextmateSnippet(this.ultisnip, this.id) ret.related.codes = this.related.codes ret.related.context = this.related.context ret._children = this.children.map(child => { let m = child.clone() m.parent = ret return m }) return ret } public walk(visitor: (marker: Marker) => boolean, ignoreChild = false): void { walk(this.children, visitor, ignoreChild) } } export class SnippetParser { constructor(private ultisnip?: boolean) { } public static escape(value: string): string { return value.replace(/\$|}|\\/g, '\\$&') } public static isPlainText(value: string): boolean { let s = new SnippetParser().parse(value.replace(/\$0$/, ''), false) return s.children.length == 1 && s.children[0] instanceof Text } private _scanner = new Scanner() private _token: Token public text(value: string): string { return this.parse(value, false).toString() } public parse(value: string, insertFinalTabstop?: boolean): TextmateSnippet { this._scanner.text(value) this._token = this._scanner.next() const snippet = new TextmateSnippet(this.ultisnip) while (this._parse(snippet)) { // nothing } // fill in values for placeholders. the first placeholder of an index // that has a value defines the value for all placeholders with that index const defaultValues = new Map() const incompletePlaceholders: Placeholder[] = [] let complexPlaceholders: Placeholder[] = [] let hasFinal = false snippet.walk(marker => { if (marker instanceof Placeholder) { if (marker.index == 0) hasFinal = true if (marker.children.some(o => o instanceof Placeholder)) { marker.primary = true complexPlaceholders.push(marker) } else if (!defaultValues.has(marker.index) && marker.children.length > 0) { marker.primary = true defaultValues.set(marker.index, marker.toString()) } else { incompletePlaceholders.push(marker) } } return true }) const complexIndexes = complexPlaceholders.map(p => p.index) for (const placeholder of incompletePlaceholders) { // avoid transform and replace since no value exists. if (defaultValues.has(placeholder.index)) { let val = defaultValues.get(placeholder.index) let text = new Text(placeholder.transform ? placeholder.transform.resolve(val) : val) placeholder.setOnlyChild(text) } else if (!complexIndexes.includes(placeholder.index)) { if (placeholder.transform) { let text = new Text(placeholder.transform.resolve('')) placeholder.setOnlyChild(text) } else { placeholder.primary = true defaultValues.set(placeholder.index, '') } } } const resolveComplex = () => { let resolved: Set = new Set() for (let p of complexPlaceholders) { if (p.children.every(o => !(o instanceof Placeholder) || defaultValues.has(o.index))) { let val = p.toString() defaultValues.set(p.index, val) for (let placeholder of incompletePlaceholders.filter(o => o.index == p.index)) { let text = new Text(placeholder.transform ? placeholder.transform.resolve(val) : val) placeholder.setOnlyChild(text) } resolved.add(p.index) } } complexPlaceholders = complexPlaceholders.filter(p => !resolved.has(p.index)) if (complexPlaceholders.length == 0 || !resolved.size) return resolveComplex() } resolveComplex() if (!hasFinal && insertFinalTabstop) { // the snippet uses placeholders but has no // final tabstop defined -> insert at the end snippet.appendChild(new Placeholder(0)) } return snippet } private _accept(type?: TokenType): boolean private _accept(type: TokenType | undefined, value: true): string private _accept(type: TokenType, value?: boolean): boolean | string { if (type === undefined || this._token.type === type) { let ret = !value ? true : this._scanner.tokenText(this._token) this._token = this._scanner.next() return ret } return false } private _backTo(token: Token): false { this._scanner.pos = token.pos + token.len this._token = token return false } private _until(type: TokenType, checkBackSlash = false): false | string { if (this._token.type === TokenType.EOF) { return false } let start = this._token let pre: Token while (this._token.type !== type || (checkBackSlash && pre && pre.type === TokenType.Backslash)) { if (checkBackSlash) pre = this._token this._token = this._scanner.next() if (this._token.type === TokenType.EOF) { return false } } let value = this._scanner.value.substring(start.pos, this._token.pos) this._token = this._scanner.next() return value } private _parse(marker: Marker): boolean { return this._parseEscaped(marker) || this._parseCodeBlock(marker) || this._parseTabstopOrVariableName(marker) || this._parseComplexPlaceholder(marker) || this._parseComplexVariable(marker) || this._parseAnything(marker) } // \$, \\, \} -> just text private _parseEscaped(marker: Marker): boolean { let value: string // eslint-disable-next-line no-cond-assign if (value = this._accept(TokenType.Backslash, true)) { // saw a backslash, append escaped token or that backslash value = this._accept(TokenType.Dollar, true) || this._accept(TokenType.CurlyClose, true) || this._accept(TokenType.Backslash, true) || (this.ultisnip && this._accept(TokenType.CurlyOpen, true)) || (this.ultisnip && this._accept(TokenType.BackTick, true)) || value marker.appendChild(new Text(value)) return true } return false } // $foo -> variable, $1 -> tabstop private _parseTabstopOrVariableName(parent: Marker): boolean { let value: string const token = this._token const match = this._accept(TokenType.Dollar) && (value = this._accept(TokenType.VariableName, true) || this._accept(TokenType.Int, true)) if (!match) { return this._backTo(token) } if (/^\d+$/.test(value)) { parent.appendChild(new Placeholder(Number(value))) } else { if (this.ultisnip && !ULTISNIP_VARIABLES.includes(value)) { parent.appendChild(new Text('$' + value)) } else { parent.appendChild(new Variable(value)) } } return true } private _checkCulybrace(marker: Marker): boolean { let count = 0 for (marker of marker.children) { if (marker instanceof Text) { let text = marker.value for (let index = 0; index < text.length; index++) { const ch = text[index] if (ch === '{') { count++ } else if (ch === '}') { count-- } } } } return count <= 0 } // ${1:}, ${1} -> placeholder private _parseComplexPlaceholder(parent: Marker): boolean { let index: string const token = this._token const match = this._accept(TokenType.Dollar) && this._accept(TokenType.CurlyOpen) && (index = this._accept(TokenType.Int, true)) if (!match) { return this._backTo(token) } const placeholder = new Placeholder(Number(index)) if (this._accept(TokenType.Colon)) { // ${1:} while (true) { const lastChar = this._scanner.isEnd() // ...} -> done if (this._accept(TokenType.CurlyClose)) { // we should consider ${1:{}} with text as {}, like ultisnip. // check if missed paried } if (!this._checkCulybrace(placeholder) && !lastChar) { placeholder.appendChild(new Text('}')) continue } parent.appendChild(placeholder) return true } if (this._parse(placeholder)) { continue } // fallback parent.appendChild(new Text('${' + index + ':')) placeholder.children.forEach(parent.appendChild, parent) return true } } else if (placeholder.index > 0 && this._accept(TokenType.Pipe)) { // ${1|one,two,three|} const choice = new Choice() while (true) { if (this._parseChoiceElement(choice)) { if (this._accept(TokenType.Comma)) { // opt, -> more continue } if (this._accept(TokenType.Pipe)) { placeholder.appendChild(choice) if (this._accept(TokenType.CurlyClose)) { // ..|} -> done parent.appendChild(placeholder) return true } } } this._backTo(token) return false } } else if (this._accept(TokenType.Forwardslash)) { // ${1///} if (this._parseTransform(placeholder)) { parent.appendChild(placeholder) return true } this._backTo(token) return false } else if (this._accept(TokenType.CurlyClose)) { // ${1} parent.appendChild(placeholder) return true } else { // ${1 <- missing curly or colon return this._backTo(token) } } private _parseChoiceElement(parent: Choice): boolean { const token = this._token const values: string[] = [] while (true) { if (this._token.type === TokenType.Comma || this._token.type === TokenType.Pipe) { break } let value: string // eslint-disable-next-line no-cond-assign if (value = this._accept(TokenType.Backslash, true)) { // \, \|, or \\ value = this._accept(TokenType.Comma, true) || this._accept(TokenType.Pipe, true) || this._accept(TokenType.Backslash, true) || value } else { value = this._accept(undefined, true) } if (!value) { // EOF this._backTo(token) return false } values.push(value) } if (values.length === 0) { this._backTo(token) return false } parent.appendChild(new Text(values.join(''))) return true } // ${foo:}, ${foo} -> variable private _parseComplexVariable(parent: Marker): boolean { let name: string const token = this._token const match = this._accept(TokenType.Dollar) && this._accept(TokenType.CurlyOpen) && (name = this._accept(TokenType.VariableName, true)) if (!match) { return this._backTo(token) } if (this.ultisnip && !ULTISNIP_VARIABLES.includes(name)) { return this._backTo(token) } const variable = new Variable(name) if (this._accept(TokenType.Colon)) { // ${foo:} while (true) { // ...} -> done if (this._accept(TokenType.CurlyClose)) { parent.appendChild(variable) return true } if (this._parse(variable)) { continue } // fallback parent.appendChild(new Text('${' + name + ':')) variable.children.forEach(parent.appendChild, parent) return true } } else if (this._accept(TokenType.Forwardslash)) { // ${foo///} if (this._parseTransform(variable)) { parent.appendChild(variable) return true } this._backTo(token) return false } else if (this._accept(TokenType.CurlyClose)) { // ${foo} parent.appendChild(variable) return true } else { // ${foo <- missing curly or colon return this._backTo(token) } } private _parseTransform(parent: TransformableMarker): boolean { // ...//} let transform = new Transform() transform.ultisnip = this.ultisnip === true let regexValue = '' let regexOptions = '' // (1) /regex while (true) { if (this._accept(TokenType.Forwardslash)) { break } let escaped: string // eslint-disable-next-line no-cond-assign if (escaped = this._accept(TokenType.Backslash, true)) { escaped = this._accept(TokenType.Forwardslash, true) || escaped regexValue += escaped continue } if (this._token.type !== TokenType.EOF) { regexValue += this._accept(undefined, true) continue } return false } // (2) /format while (true) { if (this._accept(TokenType.Forwardslash)) { break } let escaped: string // eslint-disable-next-line no-cond-assign if (escaped = this._accept(TokenType.Backslash, true)) { escaped = this._accept(TokenType.Backslash, true) || this._accept(TokenType.Forwardslash, true) || escaped transform.appendChild(new Text(escaped)) continue } if (this._parseFormatString(transform) || this._parseConditionString(transform) || this._parseAnything(transform)) { continue } return false } let ascii = false // (3) /option while (true) { if (this._accept(TokenType.CurlyClose)) { break } if (this._token.type !== TokenType.EOF) { let c = this._accept(undefined, true) if (c == 'a') { ascii = true } else { if (!knownRegexOptions.includes(c)) { logger.error(`Unknown regex option: ${c}`) } regexOptions += c } continue } return false } try { if (ascii) transform.ascii = true if (this.ultisnip) regexValue = convertRegex(regexValue) transform.regexp = new RegExp(regexValue, regexOptions) } catch (e) { return false } parent.transform = transform return true } private _parseConditionString(parent: Transform): boolean { if (!this.ultisnip) return false const token = this._token // (?1:foo:bar) if (!this._accept(TokenType.OpenParen)) { return false } if (!this._accept(TokenType.QuestionMark)) { this._backTo(token) return false } let index = this._accept(TokenType.Int, true) if (!index) { this._backTo(token) return false } if (!this._accept(TokenType.Colon)) { this._backTo(token) return false } let text = this._until(TokenType.CloseParen, true) // TODO parse ConditionMarker for ultisnip if (text) { let i = 0 while (i < text.length) { let t = text[i] if (t == ':' && text[i - 1] != '\\') { break } i++ } let ifValue = text.slice(0, i) let elseValue = text.slice(i + 1) parent.appendChild(new ConditionString(Number(index), ifValue, elseValue)) return true } this._backTo(token) return false } private _parseFormatString(parent: Transform): boolean { const token = this._token if (!this._accept(TokenType.Dollar)) { return false } let complex = false if (this._accept(TokenType.CurlyOpen)) { complex = true } let index = this._accept(TokenType.Int, true) if (!index) { this._backTo(token) return false } else if (!complex) { // $1 parent.appendChild(new FormatString(Number(index))) return true } else if (this._accept(TokenType.CurlyClose)) { // ${1} parent.appendChild(new FormatString(Number(index))) return true } else if (!this._accept(TokenType.Colon)) { this._backTo(token) return false } if (this.ultisnip) { this._backTo(token) return false } if (this._accept(TokenType.Forwardslash)) { // ${1:/upcase} let shorthand = this._accept(TokenType.VariableName, true) if (!shorthand || !this._accept(TokenType.CurlyClose)) { this._backTo(token) return false } else { parent.appendChild(new FormatString(Number(index), shorthand)) return true } } else if (this._accept(TokenType.Plus)) { // ${1:+} let ifValue = this._until(TokenType.CurlyClose) if (ifValue) { parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined)) return true } } else if (this._accept(TokenType.Dash)) { // ${2:-} let elseValue = this._until(TokenType.CurlyClose) if (elseValue) { parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue)) return true } } else if (this._accept(TokenType.QuestionMark)) { // ${2:?:} let ifValue = this._until(TokenType.Colon) if (ifValue) { let elseValue = this._until(TokenType.CurlyClose) if (elseValue) { parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue)) return true } } } else { let elseValue = this._until(TokenType.CurlyClose) if (elseValue) { parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue)) return true } } this._backTo(token) return false } private _parseCodeBlock(parent: Marker): boolean { if (!this.ultisnip) return false const token = this._token if (!this._accept(TokenType.BackTick)) { return false } let text = this._until(TokenType.BackTick, true) // `shell code` `!v` `!p` if (text) { if (!text.startsWith('!')) { let marker = new CodeBlock(text.trim(), 'shell') parent.appendChild(marker) return true } if (text.startsWith('!v')) { let marker = new CodeBlock(text.slice(2).trim(), 'vim') parent.appendChild(marker) return true } if (text.startsWith('!p')) { let code = text.slice(2) if (code.indexOf('\n') == -1) { let marker = new CodeBlock(code.trim(), 'python') parent.appendChild(marker) } else { let codes = code.split(/\r?\n/) codes = codes.filter(s => !/^\s*$/.test(s)) if (!codes.length) return true // format multi line code let ind = codes[0].match(/^\s*/)[0] if (ind.length && codes.every(s => s.startsWith(ind))) { codes = codes.map(s => s.slice(ind.length)) } if (ind == ' ' && codes[0].startsWith(ind)) codes[0] = codes[0].slice(1) let marker = new CodeBlock(codes.join('\n'), 'python') parent.appendChild(marker) } return true } } this._backTo(token) return false } private _parseAnything(marker: Marker): boolean { if (this._token.type !== TokenType.EOF) { let text = this._scanner.tokenText(this._token) marker.appendChild(new Text(text)) this._accept(undefined) return true } return false } } const escapedCharacters = [':', '(', ')', '{', '}'] // \u \l \U \L \E \n \t export function transformEscapes(input: string, backslashIndexes = []): string { let res = '' let len = input.length let i = 0 let toUpper = false let toLower = false while (i < len) { let ch = input[i] if (ch.charCodeAt(0) === CharCode.Backslash && !backslashIndexes.includes(i)) { let next = input[i + 1] if (escapedCharacters.includes(next)) { i++ continue } if (next == 'u' || next == 'l') { // Uppercase/Lowercase next letter let follow = input[i + 2] if (follow) res = res + (next == 'u' ? follow.toUpperCase() : follow.toLowerCase()) i = i + 3 continue } if (next == 'U' || next == 'L') { // Uppercase/Lowercase to \E if (next == 'U') { toUpper = true } else { toLower = true } i = i + 2 continue } if (next == 'E') { toUpper = false toLower = false i = i + 2 continue } if (next == 'n') { res += '\n' i = i + 2 continue } if (next == 't') { res += '\t' i = i + 2 continue } } if (toUpper) { ch = ch.toUpperCase() } else if (toLower) { ch = ch.toLowerCase() } res += ch i++ } return res } // merge adjacent Texts of marker's children export function mergeTexts(marker: Marker, begin = 0): void { let { children } = marker let end: number | undefined let start: number for (let i = begin; i < children.length; i++) { let m = children[i] if (m instanceof Text) { if (start !== undefined) { end = i } else { start = i } } else { if (end !== undefined) { break } start = undefined } } if (end === undefined) return let newText = '' for (let i = start; i <= end; i++) { newText += children[i].toString() } let m = new Text(newText) children.splice(start, end - start + 1, m) m.parent = marker return mergeTexts(marker, start + 1) } export function getPlaceholderId(p: Placeholder): number { if (typeof p.id === 'number') return p.id p.id = id++ return p.id } ================================================ FILE: src/snippets/session.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Position, Range, StringValue, TextEdit } from 'vscode-languageserver-types' import events from '../events' import { createLogger } from '../logger' import Document from '../model/document' import { LinesTextDocument } from '../model/textdocument' import { DidChangeTextDocumentParams, JumpInfo, TextDocumentContentChange, UltiSnippetOption } from '../types' import { defaultValue, waitNextTick } from '../util' import { getTextEdit } from '../util/diff' import { onUnexpectedError } from '../util/errors' import { omit } from '../util/lodash' import { Mutex } from '../util/mutex' import { equals } from '../util/object' import { comparePosition, emptyRange, getEnd, positionInRange, rangeInRange } from '../util/position' import { CancellationTokenSource, Emitter, Event } from '../util/protocol' import { byteIndex } from '../util/string' import { filterSortEdits, reduceTextEdit } from '../util/textedit' import window from '../window' import workspace from '../workspace' import { executePythonCode, generateContextId, getInitialPythonCode } from './eval' import { getPlaceholderId, Placeholder, Text, TextmateSnippet } from './parser' import { CocSnippet, CocSnippetPlaceholder, getNextPlaceholder, getUltiSnipActionCodes } from "./snippet" import { SnippetString } from './string' import { toSnippetString, UltiSnippetContext, wordsSource } from './util' import { SnippetVariableResolver } from "./variableResolve" const logger = createLogger('snippets-session') const NAME_SPACE = 'snippets' interface DocumentChange { version: number change: TextDocumentContentChange } export interface SnippetEdit { range: Range snippet: string | SnippetString | StringValue } export interface SnippetConfig { readonly highlight: boolean readonly nextOnDelete: boolean readonly preferComplete: boolean } export class SnippetSession { public mutex = new Mutex() private current: Placeholder private textDocument: LinesTextDocument private tokenSource: CancellationTokenSource private _applying = false private _paused = false public snippet: CocSnippet = null private _onActiveChange = new Emitter() private _selected = false public readonly onActiveChange: Event = this._onActiveChange.event constructor( private nvim: Neovim, public readonly document: Document, private readonly config: SnippetConfig ) { } public get selected(): boolean { return this._selected } public async insertSnippetEdits(edits: SnippetEdit[]): Promise { if (edits.length === 0) return this.isActive if (edits.length === 1) return await this.start(toSnippetString(edits[0].snippet), edits[0].range, false) const textDocument = this.document.textDocument const textEdits = filterSortEdits(textDocument, edits.map(e => TextEdit.replace(e.range, toSnippetString(e.snippet)))) const len = textEdits.length const snip = new TextmateSnippet() for (let i = 0; i < len; i++) { let range = textEdits[i].range let placeholder = new Placeholder(i + 1) placeholder.appendChild(new Text(textDocument.getText(range))) snip.appendChild(placeholder) if (i != len - 1) { let r = Range.create(range.end, textEdits[i + 1].range.start) snip.appendChild(new Text(textDocument.getText(r))) } } this.deactivate() const resolver = new SnippetVariableResolver(this.nvim, workspace.workspaceFolderControl) let snippet = new CocSnippet(snip, textEdits[0].range.start, this.nvim, resolver) await snippet.init() this.activate(snippet) // reverse insert needed for (let i = len - 1; i >= 0; i--) { let idx = i + 1 this.current = snip.placeholders.find(o => o.index === idx) let edit = textEdits[i] await this.start(edit.newText, edit.range, false) } return this.isActive } public async start(inserted: string, range: Range, select = true, context?: UltiSnippetContext): Promise { let { document, snippet } = this this._paused = false const edits: TextEdit[] = [] let textmateSnippet: TextmateSnippet if (inserted.length === 0) return this.isActive if (snippet && rangeInRange(range, snippet.range)) { // update all snippet. let oldRange = snippet.range let previous = snippet.text textmateSnippet = await this.snippet.replaceWithSnippet(range, inserted, this.current, context) let edit = reduceTextEdit({ range: oldRange, newText: this.snippet.text }, previous) edits.push(edit) } else { this.deactivate() const resolver = new SnippetVariableResolver(this.nvim, workspace.workspaceFolderControl) snippet = new CocSnippet(inserted, range.start, this.nvim, resolver) await snippet.init(context) textmateSnippet = snippet.tmSnippet edits.push(TextEdit.replace(range, snippet.text)) // try fix indent of text after snippet when insert new line if (inserted.replace(/\$0$/, '').endsWith('\n')) { const currentLine = document.getline(range.start.line) const remain = currentLine.slice(range.end.character) if (remain.length) { let s = range.end.character let l = remain.match(/^\s*/)[0].length let r = Range.create(range.end.line, s, range.end.line, s + l) edits.push(TextEdit.replace(r, currentLine.match(/^\s*/)[0])) } } } this.current = textmateSnippet.first this.nvim.call('coc#compat#del_var', ['coc_selected_text'], true) await this.applyEdits(edits) this.activate(snippet) // Not delay, avoid unexpected character insert if (context) await this.tryPostExpand(textmateSnippet) let { placeholder } = this if (select && placeholder) await this.selectPlaceholder(placeholder, true) return this.isActive } private async tryPostExpand(textmateSnippet: TextmateSnippet): Promise { let result = getUltiSnipActionCodes(textmateSnippet, 'postExpand') if (!result) return const { start, end } = this.snippet.range const [code, resetCodes] = result let pos = `[${start.line},${start.character},${end.line},${end.character}]` let codes = [...resetCodes, `snip = coc_ultisnips_dict["PostExpandContext"](${pos})`, code] this.cancel() await executePythonCode(this.nvim, codes) await this.forceSynchronize() } private async tryPostJump(code: string, resetCodes: string[], info: JumpInfo, bufnr: number): Promise { // make events.requesting = false await waitNextTick() this.nvim.setVar('coc_ultisnips_tabstops', info.tabstops, true) const { snippet_start, snippet_end } = info let pos = `[${snippet_start.line},${snippet_start.character},${snippet_end.line},${snippet_end.character}]` let codes = [...resetCodes, `snip = coc_ultisnips_dict["PostJumpContext"](${pos},${info.index},${info.forward ? 1 : 0})`, code] this.cancel() await executePythonCode(this.nvim, codes) await this.forceSynchronize() void events.fire('PlaceholderJump', [bufnr, info]) } public async removeWhiteSpaceBefore(placeholder: CocSnippetPlaceholder): Promise { if (!emptyRange(placeholder.range)) return let pos = placeholder.range.start let line = this.document.getline(pos.line) let ms = line.match(/\s+$/) if (ms && line.length === pos.character) { let startCharacter = pos.character - ms[0].length let textEdit = TextEdit.del(Range.create(pos.line, startCharacter, pos.line, pos.character)) await this.document.applyEdits([textEdit]) await this.forceSynchronize() } } private async applyEdits(edits: TextEdit[], joinundo = false): Promise { let { document } = this this._applying = true await document.applyEdits(edits, joinundo) this._applying = false this.textDocument = document.textDocument } public async nextPlaceholder(): Promise { await this.forceSynchronize() if (!this.current) return let marker = this.current if (this.snippet.getUltiSnipOption(marker, 'removeWhiteSpace')) { let { placeholder } = this if (placeholder) await this.removeWhiteSpaceBefore(placeholder) } const p = this.snippet.getPlaceholderOnJump(marker, true) await this.selectPlaceholder(p, true) } public async previousPlaceholder(): Promise { await this.forceSynchronize() if (!this.current) return const p = this.snippet.getPlaceholderOnJump(this.current, false) await this.selectPlaceholder(p, true, false) } public async selectCurrentPlaceholder(triggerAutocmd = true): Promise { await this.forceSynchronize() let { placeholder } = this if (placeholder) await this.selectPlaceholder(placeholder, triggerAutocmd) } public async selectPlaceholder(placeholder: CocSnippetPlaceholder | undefined, triggerAutocmd = true, forward = true): Promise { let { nvim, document } = this if (!document || !placeholder) return this._selected = true let { start, end } = placeholder.range const line = document.getline(start.line) const marker = this.current = placeholder.marker const range = this.snippet.getSnippetRange(marker) const tabstops = this.snippet.getSnippetTabstops(marker) if (marker instanceof Placeholder && marker.choice && marker.choice.options.length) { const col = byteIndex(line, start.character) + 1 wordsSource.words = marker.choice.options.map(o => o.value) wordsSource.startcol = col - 1 // pum not work when use request during request. nvim.call('coc#snippet#show_choices', [start.line + 1, col, end, placeholder.value], true) } else { await this.select(placeholder) this.highlights() } if (triggerAutocmd) nvim.call('coc#util#do_autocmd', ['CocJumpPlaceholder'], true) let info: JumpInfo = { forward, tabstops, snippet_start: range.start, snippet_end: range.end, index: placeholder.index, range: placeholder.range, charbefore: start.character == 0 ? '' : line.slice(start.character - 1, start.character) } let result = getUltiSnipActionCodes(marker, 'postJump') if (result) { this.tryPostJump(result[0], result[1], info, document.bufnr).catch(onUnexpectedError) } else { void events.fire('PlaceholderJump', [document.bufnr, info]) } this.checkFinalPlaceholder() } public checkFinalPlaceholder(): void { let current = this.current if (current && current.index === 0) { const { snippet } = current if (snippet === this.snippet.tmSnippet) { logger.info('Jump to final placeholder, cancelling snippet session') this.deactivate() } else { let marker = snippet.parent this.snippet.deactivateSnippet(snippet) if (marker instanceof Placeholder) { this.current = marker } } } } private highlights(): void { let { current, config } = this if (!current || !config.highlight || events.bufnr !== this.bufnr) return let buf = this.document.buffer this.nvim.pauseNotification() buf.clearNamespace(NAME_SPACE) let ranges = this.snippet.getRanges(current) buf.highlightRanges(NAME_SPACE, 'CocSnippetVisual', ranges) this.nvim.resumeNotification(true, true) } private async select(placeholder: CocSnippetPlaceholder): Promise { let { range, value } = placeholder let { nvim } = this if (value.length > 0) { await nvim.call('coc#snippet#select', [range.start, range.end, value]) } else { await nvim.call('coc#snippet#move', [range.start]) } nvim.redrawVim() } public async checkPosition(): Promise { if (!this.isActive) return let position = await window.getCursorPosition() if (this.snippet && positionInRange(position, this.snippet.range) != 0) { logger.info('Cursor insert out of range, cancelling snippet session') this.deactivate() } } public onTextChange(): void { this.cancel() } public onChange(e: DidChangeTextDocumentParams): void { if (this._applying || !this.isActive || this._paused) return let changes = e.contentChanges // if not cancel, applyEdits would change latest document lines, which could be wrong. this.cancel() this.synchronize({ version: e.textDocument.version, change: changes[0] }).catch(onUnexpectedError) } public async synchronize(change?: DocumentChange): Promise { const { document, isActive } = this this._paused = false if (!isActive) return await this.mutex.use(() => { if (!document.attached || document.dirty || !this.snippet || !this.textDocument || document.version === this.version) return Promise.resolve() if (change && (change.version - this.version !== 1 || document.version != change.version)) { // can't be used any more change = undefined } return this._synchronize(change) }) } public async _synchronize(documentChange?: DocumentChange): Promise { let { document, textDocument, current, snippet } = this const newDocument = document.textDocument if (equals(textDocument.lines, newDocument.lines)) { this.textDocument = newDocument return } const startTs = Date.now() let tokenSource = this.tokenSource = new CancellationTokenSource() const cursor = events.bufnr == document.bufnr ? await window.getCursorPosition() : undefined if (tokenSource.token.isCancellationRequested) return let change = documentChange?.change if (!change) { let edit = getTextEdit(textDocument.lines, newDocument.lines, cursor, events.insertMode) change = { range: edit.range, text: edit.newText } } const { range, start } = snippet let c = comparePosition(change.range.start, range.end) // consider insert at the end let insertEnd = emptyRange(change.range) && snippet.hasEndPlaceholder // change after snippet, do nothing if (c > 0 || (c === 0 && !insertEnd)) { logger.info('Content change after snippet') this.textDocument = newDocument return } // consider insert at the beginning, exclude new lines before. c = comparePosition(change.range.end, range.start) let insertBeginning = emptyRange(change.range) && !change.text.endsWith('\n') && snippet.hasBeginningPlaceholder if (c < 0 || (c === 0 && !insertBeginning)) { // change before beginning, reset position let changeEnd = change.range.end let checkCharacter = range.start.line === changeEnd.line let newLines = change.text.split(/\n/) let lc = newLines.length - (changeEnd.line - change.range.start.line + 1) let cc = 0 if (checkCharacter) { if (newLines.length > 1) { cc = newLines[newLines.length - 1].length - changeEnd.character } else { cc = change.range.start.character + change.text.length - changeEnd.character } } this.snippet.resetStartPosition(Position.create(start.line + lc, start.character + cc)) this.textDocument = newDocument logger.info('Content change before snippet, reset snippet position') return } if (!rangeInRange(change.range, range)) { logger.info('Before and snippet body changed, cancel snippet session') this.deactivate() return } const nextPlaceholder = getNextPlaceholder(current, true) const id = getPlaceholderId(current) const res = await this.snippet.replaceWithText(change.range, change.text, tokenSource.token, current, cursor) this.tokenSource = undefined if (!res) { if (this.snippet) { // find out the cloned placeholder let marker = this.snippet.getPlaceholderById(id, current.index) // the current could be invalid, so not able to find a cloned placeholder. this.current = defaultValue(marker, this.snippet.tmSnippet.first) } return } this.textDocument = newDocument let { snippetText, delta } = res let changedRange = Range.create(start, getEnd(start, snippetText)) // check if snippet not changed as expected const expected = newDocument.getText(changedRange) if (expected !== snippetText) { logger.error(`Something went wrong with the snippet implementation`, change, snippetText, expected) this.deactivate() return } let newText = this.snippet.text // further update caused by related placeholders or python CodeBlock change if (newText !== snippetText) { let edit = reduceTextEdit({ range: changedRange, newText }, snippetText) await this.applyEdits([edit], true) if (delta) { this.nvim.call(`coc#cursor#move_to`, [cursor.line + delta.line, cursor.character + delta.character], true) } } this.highlights() logger.debug('update cost:', Date.now() - startTs, res.delta) this.trySelectNextOnDelete(current, nextPlaceholder).catch(onUnexpectedError) return } public async trySelectNextOnDelete(curr: Placeholder, next: Placeholder | undefined): Promise { if (!this.config.nextOnDelete || !this.snippet || !curr || (curr.snippet != null && curr.toString() != '') || !next ) return let p = this.snippet.getPlaceholderByMarker(next) // the placeholder could be removed if (p) await this.selectPlaceholder(p, true) } public async forceSynchronize(): Promise { if (this.isActive) { this._paused = false await this.document.patchChange() await this.synchronize() } else { await this.document.patchChange() } } public async onCompleteDone(): Promise { if (this.isActive) { this._paused = false this.document._forceSync() await this.synchronize() } } public get version(): number { return this.textDocument ? this.textDocument.version : -1 } public get isActive(): boolean { return this.snippet != null } public get bufnr(): number { return this.document.bufnr } private activate(snippet: CocSnippet): void { if (this.isActive) return this.snippet = snippet this.nvim.call('coc#snippet#enable', [this.bufnr, this.config.preferComplete ? 1 : 0], true) this._onActiveChange.fire(true) } public deactivate(): void { this.cancel() if (!this.isActive) return this.snippet = null this.current = null this.nvim.call('coc#snippet#disable', [this.bufnr], true) if (this.config.highlight) this.nvim.call('coc#highlight#clear_highlight', [this.bufnr, NAME_SPACE, 0, -1], true) this._onActiveChange.fire(false) logger.debug(`session ${this.bufnr} deactivate`) } public get placeholder(): CocSnippetPlaceholder | undefined { if (!this.snippet || !this.current) return undefined return this.snippet.getPlaceholderByMarker(this.current) } public cancel(pause = false): void { if (!this.isActive) return if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource.dispose() this.tokenSource = null } if (pause) this._paused = true } public dispose(): void { this.cancel() this._onActiveChange.dispose() this.snippet = null this.current = null this.textDocument = undefined } public async resolveSnippet(nvim: Neovim, snippetString: string, ultisnip?: UltiSnippetOption): Promise { let context: UltiSnippetContext if (ultisnip) { // avoid all actions ultisnip = omit(ultisnip, ['actions']) context = Object.assign({ range: Range.create(0, 0, 0, 0), line: '' }, ultisnip, { id: generateContextId(events.bufnr) }) if (ultisnip.noPython !== true && snippetString.includes('`!p')) { await executePythonCode(nvim, getInitialPythonCode(context)) } } const resolver = new SnippetVariableResolver(nvim, workspace.workspaceFolderControl) const snippet = new CocSnippet(snippetString, Position.create(0, 0), nvim, resolver) await snippet.init(context) return snippet.text } } ================================================ FILE: src/snippets/snippet.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { Position, Range } from 'vscode-languageserver-types' import { LinesTextDocument } from '../model/textdocument' import { TabStopInfo } from '../types' import { defaultValue, waitWithToken } from '../util' import { adjacentPosition, comparePosition, emptyRange, getEnd, positionInRange, rangeInRange, samePosition } from '../util/position' import { CancellationToken } from '../util/protocol' import { getPyBlockCode, getResetPythonCode, hasPython } from './eval' import { Marker, mergeTexts, Placeholder, SnippetParser, Text, TextmateSnippet, VariableResolver } from "./parser" import { getAction, getNewRange, getTextAfter, getTextBefore, UltiSnippetContext, UltiSnipsAction, UltiSnipsOption } from './util' export interface ParentInfo { marker: TextmateSnippet | Placeholder range: Range } export interface CocSnippetPlaceholder { index: number marker: Placeholder value: string primary: boolean // range in current buffer range: Range } export interface CocSnippetInfo { marker: TextmateSnippet value: string range: Range } export interface ChangedInfo { // The changed marker marker: Marker // snippet text with only changed marker changed snippetText: string delta?: Position } export interface CursorDelta { // the line change cursor should move line: number // the character count cursor should move character: number } export class CocSnippet { // placeholders and snippets from top to bottom private _markerSequence: (Placeholder | TextmateSnippet)[] = [] private _placeholders: CocSnippetPlaceholder[] = [] // from upper to lower private _snippets: CocSnippetInfo[] = [] private _text: string private _tmSnippet: TextmateSnippet constructor( private snippet: string | TextmateSnippet, private position: Position, private nvim: Neovim, private resolver?: VariableResolver, ) { } public get tmSnippet(): TextmateSnippet { return this._tmSnippet } public get snippets(): TextmateSnippet[] { return this._snippets.map(o => o.marker) } private getSnippet(marker: Marker): TextmateSnippet | undefined { return marker instanceof TextmateSnippet ? marker : marker.snippet } public deactivateSnippet(snip: TextmateSnippet | undefined): void { if (!snip) return let marker = snip.parent if (marker) { let text = new Text(snip.toString()) snip.replaceWith(text) this.synchronize() } } public getUltiSnipOption(marker: Marker, key: UltiSnipsOption): boolean | undefined { let snip = this.getSnippet(marker) if (!snip) return undefined let context = snip.related.context if (!context) return undefined return context[key] } public async init(ultisnip?: UltiSnippetContext): Promise { if (typeof this.snippet === 'string') { const parser = new SnippetParser(!!ultisnip) const snippet = parser.parse(this.snippet, true) this._tmSnippet = snippet } else { this._tmSnippet = this.snippet } await this.resolve(this._tmSnippet, ultisnip) this.synchronize() } private async resolve(snippet: TextmateSnippet, ultisnip?: UltiSnippetContext): Promise { let { resolver, nvim } = this if (resolver) await snippet.resolveVariables(resolver) if (ultisnip) { let pyCodes: string[] = [] snippet.related.context = ultisnip if (ultisnip.noPython !== true) { if (snippet.hasPythonBlock) { pyCodes = getPyBlockCode(ultisnip) } else if (hasPython(ultisnip)) { pyCodes = getResetPythonCode(ultisnip) } if (pyCodes.length > 0) { snippet.related.codes = pyCodes } } // Code from getSnippetPythonCode already executed before snippet insert await snippet.evalCodeBlocks(nvim, pyCodes) } } public getPlaceholderOnJump(current: Placeholder | undefined, forward: boolean): CocSnippetPlaceholder | undefined { const p = getNextPlaceholder(current, forward) return p ? this.getPlaceholderByMarker(p) : undefined } /** * Same index and in same snippet only */ public getRanges(marker: Placeholder): Range[] { if (marker.toString().length === 0 || !marker.snippet) return [] let tmSnippet = marker.snippet let placeholders = this._placeholders.filter(o => o.index == marker.index && o.marker.snippet === tmSnippet) return placeholders.map(o => o.range).filter(r => !emptyRange(r)) } /** * Find the most possible marker contains range, throw error when not found */ public findParent(range: Range, current?: Placeholder): ParentInfo { const isInsert = emptyRange(range) let marker: TextmateSnippet | Placeholder let markerRange: Range const { _snippets, _placeholders, _markerSequence } = this const seq = _markerSequence.filter(o => o !== current) if (current && _markerSequence.includes(current)) seq.push(current) const list = seq.map(m => { return m instanceof TextmateSnippet ? _snippets.find(o => o.marker === m) : _placeholders.find(o => o.marker === m) }) for (let index = list.length - 1; index >= 0; index--) { const o = list[index] if (rangeInRange(range, o.range)) { // Gives choice and final placeholder lower priority for text insert if (isInsert && o.marker instanceof Placeholder && (o.marker.choice || o.marker.index === 0) && o.marker !== current && adjacentPosition(range.start, o.range) ) { continue } marker = o.marker markerRange = o.range break } } if (!marker) throw new Error(`Unable to find parent marker in range ${JSON.stringify(range, null, 2)}`) return { marker, range: markerRange } } /** * The change must happens with same marker parents, return the changed marker */ public replaceWithMarker(range: Range, marker: Marker, current?: Placeholder): Marker { // the range should already inside this.range const isInsert = emptyRange(range) const result = this.findParent(range, current) let parentMarker = result.marker let parentRange = result.range // search children need to be replaced const children = parentMarker.children let pos = parentRange.start let startIdx = 0 let deleteCount = 0 const { start, end } = range let startMarker: Marker | undefined let endMarker: Marker | undefined let preText = '' let afterText = '' let len = children.length for (let i = 0; i < len; i++) { let child = children[i] let value = child.toString() let s = Position.create(pos.line, pos.character) let e = getEnd(s, value) let r = Range.create(s, e) // Not include start position at the end of marker if (startMarker === undefined && positionInRange(start, r) === 0 && !samePosition(start, e)) { startMarker = child startIdx = i preText = getTextBefore(Range.create(s, e), value, start) // avoid delete when insert at the beginning if (isInsert && samePosition(end, s)) { endMarker = child break } } if (startMarker != null) { let val = positionInRange(end, r) if (val === 0) { endMarker = child afterText = getTextAfter(Range.create(s, e), value, end) } deleteCount += 1 } else if (i == len - 1 && samePosition(start, e)) { // insert at the end startIdx = len } if (endMarker != null) break pos = e } if (marker instanceof Text) { let newText = new Text(preText + marker.value + afterText) // Placeholder have to contain empty Text parentMarker.children.splice(startIdx, deleteCount, newText) newText.parent = parentMarker mergeTexts(parentMarker, 0) // Placeholder should not have line break at the beginning if (parentMarker instanceof Placeholder && parentMarker.children[0] instanceof Text) { let text = parentMarker.children[0] if (text.value.startsWith('\n')) { text.replaceWith(new Text(text.value.slice(1))) parentMarker.insertBefore('\n') } } } else { let markers: Marker[] = [] if (preText) markers.push(new Text(preText)) if (parentMarker instanceof TextmateSnippet) { // create a new Placeholder to make it selectable by jump let p = new Placeholder((current ? current.index : 0) + Math.random()) p.appendChild(marker) p.primary = true markers.push(p) } else { markers.push(marker) } if (afterText) markers.push(new Text(afterText)) children.splice(startIdx, deleteCount, ...markers) markers.forEach(m => m.parent = parentMarker) if (preText.length > 0 || afterText.length > 0) { mergeTexts(parentMarker, 0) } } if (parentMarker instanceof Placeholder && !parentMarker.primary) { let first = parentMarker.children[0] // Replace with Text if (parentMarker.children.length === 1 && first instanceof Text) { parentMarker.replaceWith(first) return first } // increase index to not synchronize if (Number.isInteger(parentMarker.index)) { parentMarker.index += 0.1 } } return parentMarker } /** * Replace range with text, return new Cursor position when cursor provided * * Get new Cursor position for synchronize update only. * The cursor position should already adjusted before call this function. */ public async replaceWithText(range: Range, text: string, token: CancellationToken, current?: Placeholder, cursor?: Position): Promise { let cloned = this._tmSnippet.clone() let marker = this.replaceWithMarker(range, new Text(text), current) let snippetText = this._tmSnippet.toString() // No need further action when only affect the top snippet. if (marker === this._tmSnippet) { this.synchronize() return { snippetText, marker } } // Try keep relative position with marker, since no more change for marker. let sp = this.getMarkerPosition(marker) let changeCharacter = sp && cursor && sp.line === cursor.line const reset = () => { this._tmSnippet = cloned this.synchronize() } token.onCancellationRequested(reset) await this.onMarkerUpdate(marker, token) if (token.isCancellationRequested) return undefined let ep = this.getMarkerPosition(marker) let delta: Position | undefined if (cursor && sp && ep) { let lc = ep.line - sp.line let cc = (changeCharacter ? ep.character - sp.character : 0) if (lc != 0 || cc != 0) delta = Position.create(lc, cc) } return { snippetText, marker, delta } } public async replaceWithSnippet(range: Range, text: string, current?: Placeholder, ultisnip?: UltiSnippetContext): Promise { let snippet = new SnippetParser(!!ultisnip).parse(text, true) // no need to move cursor, there should be placeholder selection afterwards. let marker = this.replaceWithMarker(range, snippet, current) await this.resolve(snippet, ultisnip) await this.onMarkerUpdate(marker, CancellationToken.None) return snippet } /** * Get placeholder or snippet start position in current document */ public getMarkerPosition(marker: Marker): Position | undefined { if (marker instanceof Placeholder) { let p = this._placeholders.find(o => o.marker === marker) return p ? p.range.start : undefined } let o = this._snippets.find(o => o.marker === marker) return o ? o.range.start : undefined } public getSnippetRange(marker: Marker): Range | undefined { let snip = marker.snippet if (!snip) return undefined let info = this._snippets.find(o => o.marker === snip) return info ? info.range : undefined } /** * Get TabStops of same snippet. */ public getSnippetTabstops(marker: Marker): TabStopInfo[] { let snip = marker.snippet if (!snip) return [] let res: TabStopInfo[] = [] this._placeholders.forEach(p => { const { start, end } = p.range if (p.marker.snippet === snip && (p.primary || p.index === 0)) { res.push({ index: p.index, range: [start.line, start.character, end.line, end.character], text: p.value }) } }) return res } public async onMarkerUpdate(marker: Marker, token: CancellationToken): Promise { let ts = Date.now() while (marker != null) { if (marker instanceof Placeholder) { let snip = marker.snippet if (!snip) break await snip.update(this.nvim, marker, token) if (token.isCancellationRequested) return marker = snip.parent } else { marker = marker.parent } } // Avoid document change fired during document change event, which may cause unexpected behavior. await waitWithToken(Math.max(0, 16 - Date.now() + ts), token) if (token.isCancellationRequested) return this.synchronize() } private usePython(snip: TextmateSnippet): boolean { return snip.hasCodeBlock || hasPython(snip.related.context) } public get hasPython(): boolean { for (const info of this._snippets) { let snip = info.marker if (this.usePython(snip)) return true } return false } public resetStartPosition(pos: Position): void { this.position = pos this.synchronize() } public get start(): Position { return Position.create(this.position.line, this.position.character) } public get range(): Range { let end = getEnd(this.position, this._text) return Range.create(this.position, end) } public get text(): string { return this._text } public get hasBeginningPlaceholder(): boolean { let { position } = this return this._placeholders.find(o => o.index !== 0 && comparePosition(o.range.start, position) === 0) != null } public get hasEndPlaceholder(): boolean { let position = this._snippets[0].range.end return this._placeholders.find(o => o.index !== 0 && comparePosition(o.range.end, position) === 0) != null } public getPlaceholderByMarker(marker: Marker): CocSnippetPlaceholder | undefined { return this._placeholders.find(o => o.marker === marker) } public getPlaceholderByIndex(index: number): CocSnippetPlaceholder { let filtered = this._placeholders.filter(o => o.index == index && !o.marker.transform) let find = filtered.find(o => o.primary) return defaultValue(find, filtered[0]) } public getPlaceholderById(id: number, index: number): Placeholder | undefined { let p = this._tmSnippet.placeholders.find(o => o.id === id) if (p) return p let placeholder = this.getPlaceholderByIndex(index) return placeholder ? placeholder.marker : undefined } /** * Should be used after snippet resolved. */ public synchronize(): void { const snippet = this._tmSnippet const snippetStr = snippet.toString() const document = new LinesTextDocument('/', '', 0, snippetStr.split(/\n/), 0, false) const placeholders: CocSnippetPlaceholder[] = [] const snippets: CocSnippetInfo[] = [] const markerSequence = [] const { start } = this snippets.push({ range: Range.create(start, getEnd(start, snippetStr)), marker: snippet, value: snippetStr }) markerSequence.push(snippet) // all placeholders, including nested placeholder from snippet let offset = 0 snippet.walk(marker => { if (marker instanceof Placeholder && marker.transform == null) { markerSequence.push(marker) const position = document.positionAt(offset) const value = marker.toString() placeholders.push({ index: marker.index, value, marker, range: getNewRange(start, position, value), primary: marker.primary === true }) } else if (marker instanceof TextmateSnippet) { markerSequence.push(marker) const position = document.positionAt(offset) const value = marker.toString() snippets.push({ range: getNewRange(start, position, value), marker, value }) } offset += marker.len() return true }, false) this._snippets = snippets this._text = snippetStr this._placeholders = placeholders this._markerSequence = markerSequence } } /** * Next or previous placeholder */ export function getNextPlaceholder(marker: Placeholder | undefined, forward: boolean, nested = false): Placeholder | undefined { if (!marker) return undefined let { snippet } = marker let idx = marker.index if (idx < 0 || !snippet) return undefined let arr: Placeholder[] = [] let min_index: number let max_index: number if (idx > 0) { snippet.walk(m => { if (m instanceof Placeholder && !m.transform) { if ( (forward && (m.index > idx || m.isFinalTabstop)) || (!forward && (m.index < idx && !m.isFinalTabstop)) ) { arr.push(m) if (!m.isFinalTabstop) { min_index = min_index === undefined ? m.index : Math.min(min_index, m.index) } max_index = max_index === undefined ? m.index : Math.max(max_index, m.index) } } return true }, true) if (arr.length > 0) { arr.sort((a, b) => { if (b.primary && !a.primary) return 1 if (a.primary && !b.primary) return -1 return 0 }) if (forward) return min_index === undefined ? arr[0] : arr.find(o => o.index === min_index) return arr.find(o => o.index === max_index) } } if (snippet.parent instanceof Placeholder) { return getNextPlaceholder(snippet.parent, forward, true) } if (nested) return marker return undefined } /** * Return action code and reset code of snippet. */ export function getUltiSnipActionCodes(marker: Marker | undefined, action: UltiSnipsAction): [string, string[]] | undefined { if (!marker) return undefined const snip = marker instanceof TextmateSnippet ? marker : marker.snippet if (!snip) return undefined let context = snip.related.context let code = getAction(context, action) if (!code) return undefined return [code, getResetPythonCode(context)] } ================================================ FILE: src/snippets/string.ts ================================================ 'use strict' export class SnippetString { public static isSnippetString(thing: any): thing is SnippetString { if (thing instanceof SnippetString) { return true } if (!thing) { return false } return typeof (thing as SnippetString).value === 'string' } private static _escape(value: string): string { return value.replace(/\$|}|\\/g, '\\$&') } private _tabstop = 1 public value: string constructor(value?: string) { this.value = value || '' } public appendText(str: string): SnippetString { this.value += SnippetString._escape(str) return this } public appendTabstop(num: number = this._tabstop++): SnippetString { this.value += '$' this.value += num return this } public appendPlaceholder(value: string | ((snippet: SnippetString) => any), num: number = this._tabstop++): SnippetString { if (typeof value === 'function') { const nested = new SnippetString() nested._tabstop = this._tabstop value(nested) this._tabstop = nested._tabstop value = nested.value } else { value = SnippetString._escape(value) } this.value += '${' this.value += num this.value += ':' this.value += value this.value += '}' return this } public appendChoice(values: string[], num: number = this._tabstop++): SnippetString { const value = values.map(s => s.replaceAll(/[|\\,]/g, '\\$&')).join(',') this.value += '${' this.value += num this.value += '|' this.value += value this.value += '|}' return this } public appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => any)): SnippetString { if (typeof defaultValue === 'function') { const nested = new SnippetString() nested._tabstop = this._tabstop defaultValue(nested) this._tabstop = nested._tabstop defaultValue = nested.value } else if (typeof defaultValue === 'string') { defaultValue = defaultValue.replace(/\$|}/g, '\\$&') // CodeQL [SM02383] I do not want to escape backslashes here } this.value += '${' this.value += name if (defaultValue) { this.value += ':' this.value += defaultValue } this.value += '}' return this } } ================================================ FILE: src/snippets/util.ts ================================================ import { Position, Range, StringValue } from 'vscode-languageserver-types' import type { CompleteOption, ExtendedCompleteItem, ISource } from '../completion/types' import { UltiSnipsActions } from '../types' import { defaultValue } from '../util' import { getEnd } from '../util/position' import { SnippetString } from './string' export type UltiSnipsAction = 'preExpand' | 'postExpand' | 'postJump' export type UltiSnipsOption = 'trimTrailingWhitespace' | 'removeWhiteSpace' | 'noExpand' export interface UltiSnippetContext { id: string /** * line on insert */ line: string /** * Range to replace, start.line should equal end.line */ range: Range /** * Context python code. */ context?: string /** * Regex trigger (python code) */ regex?: string /** * Avoid python code eval when is true. */ noPython?: boolean /** * Do not expand tabs */ noExpand?: boolean /** * Trim all whitespaces from right side of snippet lines. */ trimTrailingWhitespace?: boolean /** * Remove whitespace immediately before the cursor at the end of a line before jumping to the next tabstop */ removeWhiteSpace?: boolean actions?: UltiSnipsActions } export interface SnippetFormatOptions { tabSize: number insertSpaces: boolean trimTrailingWhitespace?: boolean // options from ultisnips context noExpand?: boolean [key: string]: boolean | number | string | undefined } const stringStartRe = /\\A/ const conditionRe = /\(\?\(\w+\).+\|/ const commentRe = /\(\?#.*?\)/ const namedCaptureRe = /\(\?P<\w+>.*?\)/ const namedReferenceRe = /\(\?P=(\w+)\)/ const regex = new RegExp(`${commentRe.source}|${stringStartRe.source}|${namedCaptureRe.source}|${namedReferenceRe.source}`, 'g') /** * Convert python regex to javascript regex, * throw error when unsupported pattern found */ export function convertRegex(str: string): string { if (str.indexOf('\\z') !== -1) { throw new Error('pattern \\z not supported') } if (str.indexOf('(?s)') !== -1) { throw new Error('pattern (?s) not supported') } if (str.indexOf('(?x)') !== -1) { throw new Error('pattern (?x) not supported') } if (str.indexOf('\n') !== -1) { throw new Error('pattern \\n not supported') } if (conditionRe.test(str)) { throw new Error('pattern (?id/name)yes-pattern|no-pattern not supported') } return str.replace(regex, (match, p1) => { if (match.startsWith('(?#')) return '' if (match.startsWith('(?P<')) return '(?' + match.slice(3) if (match.startsWith('(?P=')) return `\\k<${p1}>` // if (match == '\\A') return '^' return '^' }) } /** * Action code from context or option */ export function getAction(opt: { actions?: { [key: string]: any } } | undefined, action: UltiSnipsAction): string | undefined { if (!opt || !opt.actions) return undefined return opt.actions[action] } export function shouldFormat(snippet: string): boolean { if (/^\s/.test(snippet)) return true if (snippet.indexOf('\n') !== -1) return true return false } export function normalizeSnippetString(snippet: string, indent: string, opts: SnippetFormatOptions): string { let lines = snippet.split(/\r?\n/) let ind = opts.insertSpaces ? ' '.repeat(opts.tabSize) : '\t' let tabSize = defaultValue(opts.tabSize, 2) let noExpand = opts.noExpand let trimTrailingWhitespace = opts.trimTrailingWhitespace lines = lines.map((line, idx) => { let space = line.match(/^\s*/)[0] let pre = space let isTab = space.startsWith('\t') if (isTab && opts.insertSpaces && !noExpand) { pre = ind.repeat(space.length) } else if (!isTab && !opts.insertSpaces) { pre = ind.repeat(space.length / tabSize) } return (idx == 0 || (trimTrailingWhitespace && line.length == 0) ? '' : indent) + pre + line.slice(space.length) }) return lines.join('\n') } /** * For static words, must be triggered by source option. * Used for completion of snippet choices. */ export class WordsSource implements ISource { public readonly name = '$words' public readonly shortcut = '' public readonly triggerOnly = true public words: string[] = [] public startcol: number | undefined public doComplete(opt: CompleteOption) { return { startcol: this.startcol, items: this.words.map(s => { return { word: s, filterText: opt.input } }) } } } export const wordsSource = new WordsSource() /** * Get range from base position and position, text */ export function getNewRange(base: Position, pos: Position, value: string): Range { const { line, character } = base const start: Position = { line: line + pos.line, character: pos.line == 0 ? character + pos.character : pos.character } return Range.create(start, getEnd(start, value)) } export function getTextBefore(range: Range, text: string, pos: Position): string { let newLines = [] let { line, character } = range.start let n = pos.line - line const lines = text.split('\n') for (let i = 0; i <= n; i++) { let line = lines[i] if (i == n) { newLines.push(line.slice(0, i == 0 ? pos.character - character : pos.character)) } else { newLines.push(line) } } return newLines.join('\n') } export function getTextAfter(range: Range, text: string, pos: Position): string { let newLines = [] let n = range.end.line - pos.line const lines = text.split('\n') let len = lines.length for (let i = 0; i <= n; i++) { let idx = len - i - 1 let line = lines[idx] if (i == n) { let sc = range.start.character let from = idx == 0 ? pos.character - sc : pos.character newLines.unshift(line.slice(from)) } else { newLines.unshift(line) } } return newLines.join('\n') } export function toSnippetString(snippet: string | SnippetString | StringValue): string { if (typeof snippet === 'string') { return snippet } if (typeof snippet.value === 'string') { return snippet.value } throw new TypeError(`Snippet should be string or has value as string`) } ================================================ FILE: src/snippets/variableResolve.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { v4 as uuid } from 'uuid' import { URI } from 'vscode-uri' import WorkspaceFolderController from '../core/workspaceFolder' import { path } from '../util/node' import { hasOwnProperty } from '../util/object' import { Variable, VariableResolver } from "./parser" export function padZero(n: number): string { return n < 10 ? '0' + n : n.toString() } export function parseComments(comments: string): { start?: string, end?: string, single?: string } { let start: string let end: string let single: string let parts = comments.split(',') for (let s of parts) { if (start && end && single) break if (!s.includes(':')) continue let [flag, str] = s.split(':') if (flag.includes('s')) { start = str } else if (flag.includes('e')) { end = str } else if (!single && flag == '') { single = str } } return { start, end, single } } /* * Get single line comment text */ export function parseCommentstring(commentstring: string): string | undefined { if (commentstring.endsWith('%s')) return commentstring.slice(0, -2).trim() return undefined } export class SnippetVariableResolver implements VariableResolver { private _variableToValue: { [key: string]: string } = {} constructor(private nvim: Neovim, private workspaceFolder: WorkspaceFolderController) { const currentDate = new Date() const fullyear = currentDate.getFullYear().toString() Object.assign(this._variableToValue, { CURRENT_YEAR: fullyear, CURRENT_YEAR_SHORT: fullyear.slice(-2), CURRENT_MONTH: padZero(currentDate.getMonth() + 1), CURRENT_DATE: padZero(currentDate.getDate()), CURRENT_HOUR: padZero(currentDate.getHours()), CURRENT_MINUTE: padZero(currentDate.getMinutes()), CURRENT_SECOND: padZero(currentDate.getSeconds()), CURRENT_DAY_NAME: currentDate.toLocaleString("en-US", { weekday: "long" }), CURRENT_DAY_NAME_SHORT: currentDate.toLocaleString("en-US", { weekday: "short" }), CURRENT_MONTH_NAME: currentDate.toLocaleString("en-US", { month: "long" }), CURRENT_MONTH_NAME_SHORT: currentDate.toLocaleString("en-US", { month: "short" }), TM_FILENAME: null, TM_FILENAME_BASE: null, TM_DIRECTORY: null, TM_FILEPATH: null, YANK: null, TM_LINE_INDEX: null, TM_LINE_NUMBER: null, TM_CURRENT_LINE: null, TM_CURRENT_WORD: null, TM_SELECTED_TEXT: null, VISUAL: null, CLIPBOARD: null, RELATIVE_FILEPATH: null, RANDOM: null, RANDOM_HEX: null, UUID: null, BLOCK_COMMENT_START: null, BLOCK_COMMENT_END: null, LINE_COMMENT: null, WORKSPACE_NAME: null, WORKSPACE_FOLDER: null }) } private async resolveValue(name: string): Promise { let { nvim } = this if (['TM_FILENAME', 'TM_FILENAME_BASE', 'TM_DIRECTORY', 'TM_FILEPATH'].includes(name)) { let filepath = await nvim.call('coc#util#get_fullpath') as string if (name === 'TM_FILENAME') return path.basename(filepath) if (name === 'TM_FILENAME_BASE') return path.basename(filepath, path.extname(filepath)) if (name === 'TM_DIRECTORY') return path.dirname(filepath) if (name === 'TM_FILEPATH') return filepath } if (name === 'YANK') { return await nvim.call('getreg', ['""']) as string } if (name === 'TM_LINE_INDEX') { let lnum = await nvim.call('line', ['.']) as number return (lnum - 1).toString() } if (name === 'TM_LINE_NUMBER') { let lnum = await nvim.call('line', ['.']) as number return lnum.toString() } if (name === 'TM_CURRENT_LINE') { return await nvim.call('getline', ['.']) as string } if (name === 'TM_CURRENT_WORD') { return await nvim.eval(`expand('')`) as string } if (name === 'TM_SELECTED_TEXT' || name == 'VISUAL') { return await nvim.eval(`get(g:,'coc_selected_text', v:null)`) as string } if (name === 'CLIPBOARD') { return await nvim.eval('@*') as string } if (name === 'RANDOM') { return Math.random().toString().slice(-6) } if (name === 'RANDOM_HEX') { return Math.random().toString(16).slice(-6) } if (name === 'UUID') { return uuid() } if (['RELATIVE_FILEPATH', 'WORKSPACE_NAME', 'WORKSPACE_FOLDER'].includes(name)) { let filepath = await nvim.call('coc#util#get_fullpath') as string let folder = this.workspaceFolder.getWorkspaceFolder(URI.file(filepath)) if (name === 'RELATIVE_FILEPATH') return this.workspaceFolder.getRelativePath(filepath) if (name === 'WORKSPACE_NAME') return folder.name if (name === 'WORKSPACE_FOLDER') return URI.parse(folder.uri).fsPath } if (name === 'LINE_COMMENT') { let commentstring = await nvim.eval('&commentstring') as string let s = parseCommentstring(commentstring) if (s) return s let comments = await nvim.eval('&comments') as string let { single } = parseComments(comments) return single } if (['BLOCK_COMMENT_START', 'BLOCK_COMMENT_END'].includes(name)) { let comments = await nvim.eval('&comments') as string let { start, end } = parseComments(comments) if (name === 'BLOCK_COMMENT_START') return start if (name === 'BLOCK_COMMENT_END') return end } } public async resolve(variable: Variable): Promise { const name = variable.name let resolved = this._variableToValue[name] if (resolved != null) return resolved.toString() // resolve known value if (hasOwnProperty(this._variableToValue, name)) { let value = await this.resolveValue(name) if (!value && variable.children.length) { return variable.toString() } return value == null ? '' : value.toString() } if (variable.children.length) return variable.toString() return undefined } } ================================================ FILE: src/tree/BasicDataProvider.ts ================================================ 'use strict' import { v4 as uuid } from 'uuid' import { CancellationToken, Disposable, Emitter, Event } from '../util/protocol' import { MarkupContent } from 'vscode-languageserver-types' import commandsManager from '../commands' import { ProviderResult } from '../provider' import { disposeAll } from '../util' import { TreeDataProvider, TreeItemAction } from './index' import { TreeItem, TreeItemCollapsibleState, TreeItemIcon, TreeItemLabel } from './TreeItem' import { toArray } from '../util/array' export interface TreeNode { label: string key?: string tooltip?: string | MarkupContent description?: string deprecated?: boolean icon?: TreeItemIcon children?: this[] } export interface ProviderOptions { provideData: () => ProviderResult expandLevel?: number onDispose?: () => void handleClick?: (item: T) => ProviderResult resolveIcon?: (item: T) => TreeItemIcon | undefined resolveItem?: (item: TreeItem, element: T, token: CancellationToken) => ProviderResult resolveActions?(item: TreeItem, element: T): ProviderResult[]> } function isIcon(obj: any): obj is TreeItemIcon { if (!obj) return false return typeof obj.text === 'string' && typeof obj.hlGroup === 'string' } /** * Check label and key, children not checked. */ function sameTreeNode(one: T, two: T): boolean { if (one.label === two.label && one.deprecated === two.deprecated && one.key === two.key) { return true } return false } /** * Check changes of nodes array, children not checked. */ function sameTreeNodes(one: T[], two: T[]): boolean { if (one.length !== two.length) return false return one.every((v, idx) => sameTreeNode(v, two[idx])) } /** * Tree data provider for resolved tree with children. * Use update() to update data. */ export default class BasicDataProvider implements TreeDataProvider { private disposables: Disposable[] = [] private invokeCommand: string private data: T[] | undefined // only fired for change of exists TreeNode private _onDidChangeTreeData = new Emitter() public onDidChangeTreeData: Event = this._onDidChangeTreeData.event public resolveActions: (item: TreeItem, element: T) => ProviderResult[]> // data is shared with TreeView constructor(private opts: ProviderOptions) { this.invokeCommand = `_invoke_${uuid()}` this.disposables.push(commandsManager.registerCommand(this.invokeCommand, async (node: T) => { await opts.handleClick(node) }, null, true)) if (typeof opts.resolveActions === 'function') { this.resolveActions = opts.resolveActions.bind(this) } } public iterate(node: T, parentNode: T | undefined, level: number, fn: (node: T, parentNode: T | undefined, level: number) => void | boolean): void | boolean { let res = fn(node, parentNode, level) if (res === false) return false if (Array.isArray(node.children)) { for (let element of node.children) { let res = this.iterate(element, node, level + 1, fn) if (res === false) return false } } return res } /** * Change old array to new nodes in place, keep old reference when possible. */ private updateNodes(old: T[], data: T[], parentNode: T | undefined, fireEvent = true): void { let sameNodes = sameTreeNodes(old, data) const applyNode = (previous: T, curr: T, fireEvent: boolean): void => { let changed = false for (let key of Object.keys(curr)) { if (['children', 'key'].includes(key)) continue previous[key] = curr[key] } if (previous.children?.length && !curr.children?.length) { // removed children delete previous.children changed = true } if (!previous.children?.length && curr.children?.length) { // new children previous.children = curr.children changed = true } if (changed) { if (fireEvent) this._onDidChangeTreeData.fire(previous) return } if (toArray(previous.children).length > 0 && toArray(curr.children).length > 0) { this.updateNodes(previous.children, curr.children, previous, fireEvent) } } if (sameNodes) { for (let i = 0; i < old.length; i++) { applyNode(old[i], data[i], fireEvent) } } else { let oldNodes = old.splice(0, old.length) let used: Set = new Set() for (let i = 0; i < data.length; i++) { let curr = data[i] let findIndex: number if (curr.key) { findIndex = oldNodes.findIndex((o, i) => !used.has(i) && o.key == curr.key) } else { findIndex = oldNodes.findIndex((o, i) => !used.has(i) && o.label == curr.label) } if (findIndex === -1) { old[i] = curr } else { used.add(findIndex) let previous = oldNodes[findIndex] applyNode(previous, curr, false) old[i] = previous } } if (fireEvent) { this._onDidChangeTreeData.fire(parentNode) } } } /** * Update with new data, fires change event when necessary. */ public update(data: T[], reset?: boolean): ReadonlyArray { if (!this.data) return if (reset) { this.data = toArray(data) this._onDidChangeTreeData.fire(undefined) } else { this.updateNodes(this.data, toArray(data), undefined) } return this.data } public getTreeItem(node: T): TreeItem { let label: string | TreeItemLabel = node.label let { expandLevel } = this.opts let item: TreeItem if (!node.children?.length) { item = new TreeItem(label) } else { if (expandLevel && expandLevel > 0) { let level = this.getLevel(node) let state = level && level <= expandLevel ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed item = new TreeItem(label, state) } else { item = new TreeItem(label, TreeItemCollapsibleState.Collapsed) } } item.description = node.description if (node.deprecated) item.deprecated = true if (node.tooltip) item.tooltip = node.tooltip if (isIcon(node.icon)) { item.icon = node.icon } else if (typeof this.opts.resolveIcon === 'function') { let res = this.opts.resolveIcon(node) if (res && isIcon(res)) item.icon = res } return item } public async getChildren(element?: T): Promise { if (element) return element.children ?? [] if (this.data) return this.data let data = await Promise.resolve(this.opts.provideData()) if (!Array.isArray(data)) throw new Error(`Unable to fetch data`) this.data = data return data } /** * Use reference check */ public getParent(element: T): T | undefined { if (!this.data) return undefined let find: T for (let item of this.data) { let res = this.iterate(item, null, 0, (node, parentNode) => { if (node === element) { find = parentNode return false } }) if (res === false) break } return find } public getLevel(element: T): number { if (!this.data) return 0 let level = 0 for (let item of toArray(this.data)) { let res = this.iterate(item, null, 1, (node, _parentNode, l) => { if (node === element) { level = l return false } }) if (res === false) break } return level } /** * Resolve command and tooltip */ public async resolveTreeItem(item: TreeItem, element: T, token: CancellationToken): Promise { if (typeof this.opts.resolveItem === 'function') { let res = await Promise.resolve(this.opts.resolveItem(item, element, token)) if (res) Object.assign(item, res) } if (!item.command) { item.command = { title: `invoke ${element.label}`, command: this.invokeCommand, arguments: [element] } } return item } public dispose(): void { this.data = [] this._onDidChangeTreeData.dispose() if (typeof this.opts.onDispose === 'function') { this.opts.onDispose() } disposeAll(this.disposables) } } ================================================ FILE: src/tree/LocationsDataProvider.ts ================================================ import { Range, SymbolKind, SymbolTag } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import commands from '../commands' import { path } from '../util/node' import { CancellationToken, CancellationTokenSource, Emitter, Event } from '../util/protocol' import workspace from '../workspace' import { TreeDataProvider, TreeItemAction } from './index' import { TreeItem, TreeItemCollapsibleState } from './TreeItem' export interface LocationDataItem { name: string kind: SymbolKind tags?: SymbolTag[] detail?: string uri: string range?: Range selectionRange?: Range parent?: T children?: T[] } interface ProviderConfig { readonly openCommand: string readonly enableTooltip: boolean } export default class LocationsDataProvider, P> implements TreeDataProvider{ private readonly _onDidChangeTreeData = new Emitter() public readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event private tokenSource: CancellationTokenSource private actions: TreeItemAction[] = [] public static rangesHighlight = 'CocSelectedRange' constructor( public meta: P, private winid: number, private config: ProviderConfig, private commandId: string, private rootItems: ReadonlyArray, private getIcon: (kind: SymbolKind) => { text: string, hlGroup: string }, private resolveChildren: (el: T, meta: P, token: CancellationToken) => Promise ) { this.addAction('Open in new tab', async element => { await commands.executeCommand(this.commandId, winid, element, 'tabe') }) this.addAction('Dismiss', async element => { if (element.parent == null) { let els = this.rootItems.filter(o => o !== element) this.reset(els) } else { let parentElement = element.parent let idx = parentElement.children.findIndex(o => o === element) parentElement.children.splice(idx, 1) this._onDidChangeTreeData.fire(parentElement) } }) } protected cancel() { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource = undefined } } public reset(rootItems: T[]): void { this.rootItems = rootItems this._onDidChangeTreeData.fire(undefined) } public addAction(title: string, handler: (element: T) => void): void { this.actions.push({ title, handler }) } public async getChildren(element?: T): Promise> { this.cancel() this.tokenSource = new CancellationTokenSource() let { token } = this.tokenSource if (!element) { for (let o of this.rootItems) { let children = await this.resolveChildren(o, this.meta, token) addChildren(o, children, token) } return this.rootItems } if (element.children) return element.children let items = await this.resolveChildren(element, this.meta, token) this.tokenSource = undefined addChildren(element, items, token) return items } public getTreeItem(element: T): TreeItem { let item = new TreeItem(element.name, element.children ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed) if (this.config.enableTooltip) { item.tooltip = path.relative(workspace.cwd, URI.parse(element.uri).fsPath) } item.description = element.detail item.deprecated = element.tags?.includes(SymbolTag.Deprecated) item.icon = this.getIcon(element.kind) item.command = { command: this.commandId, title: 'open location', arguments: [this.winid, element, this.config.openCommand] } return item } public resolveActions(): TreeItemAction[] { return this.actions } public dispose(): void { this.cancel() let win = workspace.nvim.createWindow(this.winid) win.clearMatchGroup(LocationsDataProvider.rangesHighlight) } } export function addChildren>(el: T, children: T[] | undefined, token?: CancellationToken): void { if (!Array.isArray(children) || (token && token.isCancellationRequested)) return children.forEach(item => item.parent = el) el.children = children } ================================================ FILE: src/tree/TreeItem.ts ================================================ 'use strict' import { Command, MarkupContent } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { path } from '../util/node' export interface TreeItemLabel { label: string highlights?: [number, number][] } // eslint-disable-next-line no-redeclare export namespace TreeItemLabel { export function is(obj: any): obj is TreeItemLabel { return typeof obj.label == 'string' } } export interface TreeItemIcon { text: string hlGroup: string } /** * Collapsible state of the tree item */ export enum TreeItemCollapsibleState { /** * Determines an item can be neither collapsed nor expanded. Implies it has no children. */ None = 0, /** * Determines an item is collapsed */ Collapsed = 1, /** * Determines an item is expanded */ Expanded = 2 } export class TreeItem { public label: string | TreeItemLabel public id?: string public description?: string public icon?: TreeItemIcon public resourceUri?: URI public command?: Command public tooltip?: string | MarkupContent public deprecated?: boolean constructor(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState) // eslint-disable-next-line @typescript-eslint/unified-signatures constructor(resourceUri: URI, collapsibleState?: TreeItemCollapsibleState) constructor(label: string | TreeItemLabel | URI, public collapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.None) { if (URI.isUri(label)) { this.resourceUri = label this.label = path.basename(label.path) this.id = label.toString() } else { this.label = label } } } export function getItemLabel(item: TreeItem): string { return TreeItemLabel.is(item.label) ? item.label.label : item.label } ================================================ FILE: src/tree/TreeView.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import { MarkupContent, MarkupKind, Range } from 'vscode-languageserver-types' import commandManager from '../commands' import type { LocalMode } from '../core/keymaps' import events from '../events' import { createLogger } from '../logger' import { toSpans } from '../model/fuzzyMatch' import { Documentation, FloatFactory, HighlightItem, IConfigurationChangeEvent } from '../types' import { defaultValue, disposeAll, getConditionValue } from '../util' import { isFalsyOrEmpty, toArray } from '../util/array' import { fuzzyScoreGracefulAggressive } from '../util/filter' import { Mutex } from '../util/mutex' import { debounce } from '../util/node' import { equals } from '../util/object' import { CancellationTokenSource, Disposable, Emitter, Event } from '../util/protocol' import { byteLength, byteSlice, toText } from '../util/string' import window from '../window' import workspace from '../workspace' import Filter, { sessionKey } from './filter' import { LineState, TreeDataProvider, TreeItemData, TreeView, TreeViewExpansionEvent, TreeViewKeys, TreeViewOptions, TreeViewSelectionChangeEvent, TreeViewVisibilityChangeEvent } from './index' import { getItemLabel, TreeItem, TreeItemCollapsibleState, TreeItemLabel } from './TreeItem' const logger = createLogger('BasicTreeView') const retryTimeout = getConditionValue(500, 10) const maxRetry = getConditionValue(5, 1) const highlightNamespace = 'tree' const signOffset = 3000 let globalId = 1 interface TreeViewConfig { openedIcon: string closedIcon: string } interface RenderedItem { line: string level: number node: T } interface ExtendedItem extends RenderedItem { index: number score: number highlights: HighlightItem[] } interface LocalKeymapDef { mode: LocalMode key: string notify: boolean fn: (element: T | undefined) => Promise | void } /** * Basic TreeView implementation */ export default class BasicTreeView implements TreeView { private bufnr: number | undefined private bufname: string private winid: number | undefined private config: TreeViewConfig private keys: TreeViewKeys private _targetBufnr: number private _targetWinId: number private _targetTabId: number | undefined private _selection: T[] = [] private _keymapDefs: LocalKeymapDef[] = [] private _onDispose = new Emitter() private _onDidRefrash = new Emitter() private _onDidExpandElement = new Emitter>() private _onDidCollapseElement = new Emitter>() private _onDidChangeSelection = new Emitter>() private _onDidChangeVisibility = new Emitter() private _onDidFilterStateChange = new Emitter() private readonly _onDidCursorMoved = new Emitter() public readonly onDidRefrash: Event = this._onDidRefrash.event public readonly onDispose: Event = this._onDispose.event public readonly onDidExpandElement: Event> = this._onDidExpandElement.event public readonly onDidCollapseElement: Event> = this._onDidCollapseElement.event public readonly onDidChangeSelection: Event> = this._onDidChangeSelection.event public readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event public readonly onDidFilterStateChange: Event = this._onDidFilterStateChange.event public readonly onDidCursorMoved: Event = this._onDidCursorMoved.event public message: string | undefined public title: string public description: string | undefined private retryTimers = 0 private renderedItems: RenderedItem[] = [] public provider: TreeDataProvider private nodesMap: Map = new Map() private mutex: Mutex = new Mutex() private timer: NodeJS.Timeout private disposables: Disposable[] = [] private tooltipFactory: FloatFactory private resolveTokenSource: CancellationTokenSource | undefined private lineState: LineState = { titleCount: 0, messageCount: 0 } private filter: Filter | undefined private filterText: string | undefined private itemsToFilter: T[] | undefined private readonly leafIndent: boolean private readonly winfixwidth: boolean private readonly autoWidth: boolean constructor(private viewId: string, private opts: TreeViewOptions) { this.loadConfiguration() workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables) if (opts.enableFilter) { this.filter = new Filter(this.nvim, [this.keys.selectNext, this.keys.selectPrevious, this.keys.invoke]) } let id = globalId globalId = globalId + 1 this.bufname = `CocTree${id}` this.tooltipFactory = window.createFloatFactory({ modes: ['n'] }) this.provider = opts.treeDataProvider this.leafIndent = opts.disableLeafIndent !== true this.winfixwidth = opts.winfixwidth !== false this.autoWidth = opts.autoWidth === true let message: string | undefined Object.defineProperty(this, 'message', { set: (msg: string | undefined) => { message = msg ? msg.replace(/\r?\n/g, ' ') : undefined this.updateHeadLines() }, get: () => { return message } }) let title = viewId.replace(/\r?\n/g, ' ') Object.defineProperty(this, 'title', { set: (newTitle: string) => { title = newTitle ? newTitle.replace(/\r?\n/g, ' ') : undefined this.updateHeadLines() }, get: () => { return title } }) let description: string | undefined Object.defineProperty(this, 'description', { set: (desc: string | undefined) => { description = desc ? desc.replace(/\r?\n/g, ' ') : undefined this.updateHeadLines() }, get: () => { return description } }) let filterText: string | undefined Object.defineProperty(this, 'filterText', { set: (text: string | undefined) => { let { titleCount, messageCount } = this.lineState let start = titleCount + messageCount if (text != null) { let highlights: HighlightItem[] = [{ lnum: start, colStart: byteLength(text), colEnd: byteLength(text) + 1, hlGroup: 'Cursor' }] this.renderedItems = [] this.updateUI([text + ' '], highlights, start, -1, true) void this.doFilter(text) } else if (filterText != null) { this.updateUI([], [], start, start + 1) } filterText = text }, get: () => { return filterText } }) if (this.provider.onDidChangeTreeData) { this.provider.onDidChangeTreeData(this.onDataChange, this, this.disposables) } events.on('BufUnload', bufnr => { if (bufnr != this.bufnr) return let isVisible = this.winid != null this.winid = undefined this.bufnr = undefined if (isVisible) this._onDidChangeVisibility.fire({ visible: false }) this.dispose() }, null, this.disposables) events.on('WinClosed', winid => { if (this.winid === winid) { this.winid = undefined this._onDidChangeVisibility.fire({ visible: false }) } }, null, this.disposables) // switched to another buffer events.on('BufWinLeave', (bufnr: number, winid: number) => { if (bufnr == this.bufnr && winid == this.winid) { this.winid = undefined this._onDidChangeVisibility.fire({ visible: false }) } }, null, this.disposables) window.onDidTabClose(id => { if (this._targetTabId === id) { this.dispose() } }, null, this.disposables) events.on('CursorHold', async (bufnr: number, cursor: [number, number]) => { if (bufnr != this.bufnr) return await this.onHover(cursor[0]) }, null, this.disposables) events.on(['CursorMoved', 'BufEnter'], () => { this.cancelResolve() }, null, this.disposables) // vim may send unexpected CursorMoved when switch tab let debounced = debounce((bufnr: number, cursor: [number, number]) => { if (bufnr !== this.bufnr) return let element = this.getElementByLnum(cursor[0] - 1) this._onDidCursorMoved.fire(element) }, 30) this.disposables.push(Disposable.create(() => { debounced.clear() })) events.on('CursorMoved', debounced, null, this.disposables) events.on('WinEnter', winid => { if (winid != this.windowId || !this.filtering) return let buf = this.nvim.createBuffer(this.bufnr) let line = this.startLnum - 1 let len = toText(this.filterText).length let range = Range.create(line, len, line, len + 1) buf.highlightRanges(highlightNamespace, 'Cursor', [range]) this.nvim.call('coc#prompt#start_prompt', [sessionKey], true) this.redraw() }, null, this.disposables) events.on('WinLeave', winid => { if (winid != this.windowId || !this.filtering) return let buf = this.nvim.createBuffer(this.bufnr) this.nvim.call('coc#prompt#stop_prompt', [sessionKey], true) buf.clearNamespace(highlightNamespace, this.startLnum - 1, this.startLnum) }, null, this.disposables) this.disposables.push(this._onDidChangeVisibility, this._onDidCursorMoved, this._onDidChangeSelection, this._onDidCollapseElement, this._onDidExpandElement) if (this.filter) { this.filter.onDidExit(node => { this.nodesMap.clear() this.filterText = undefined this.itemsToFilter = undefined if (node && typeof this.provider.getParent === 'function') { this.renderedItems = [] void this.reveal(node, { focus: true }) } else { this.clearSelection() void this.render() } this._onDidFilterStateChange.fire(false) }) this.filter.onDidUpdate(text => { this.filterText = text }) this.filter.onDidKeyPress(async character => { let items = toArray(this.renderedItems) let curr = this.selection[0] if (character == '' || character == this.keys.selectPrevious) { let idx = items.findIndex(o => o.node == curr) let index = idx == -1 || idx == 0 ? items.length - 1 : idx - 1 let node = items[index]?.node if (node) this.selectItem(node, true) } if (character == '' || character == this.keys.selectNext) { let idx = items.findIndex(o => o.node == curr) let index = idx == -1 || idx == items.length - 1 ? 0 : idx + 1 let node = items[index]?.node if (node) this.selectItem(node, true) } if (character == '' || character == this.keys.invoke) { if (!curr) return await this.invokeCommand(curr) this.filter.deactivate(curr) } }) } } public get windowId(): number | undefined { return this.winid } public get targetTabId(): number | undefined { return this._targetTabId } public get targetWinId(): number | undefined { return this._targetWinId } public get targetBufnr(): number | undefined { return this._targetBufnr } private get startLnum(): number { let filterCount = this.filterText == null ? 0 : 1 return this.lineState.messageCount + this.lineState.titleCount + filterCount } private get nvim(): Neovim { return workspace.nvim } public get filtering(): boolean { return this.filter != null && this.filter.activated } private loadConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('tree')) { let config = workspace.getConfiguration('tree', null) this.config = { openedIcon: config.get('openedIcon', ' '), closedIcon: config.get('closedIcon', ' ') } this.keys = { close: config.get('key.close'), invoke: config.get('key.invoke'), toggle: config.get('key.toggle'), actions: config.get('key.actions'), collapseAll: config.get('key.collapseAll'), toggleSelection: config.get('key.toggleSelection'), activeFilter: config.get('key.activeFilter'), selectNext: config.get('key.selectNext'), selectPrevious: config.get('key.selectPrevious') } if (e && this.visible) { void this.render() } } } private async doFilter(text: string): Promise { let items: ExtendedItem[] = [] let index = 0 let release = await this.mutex.acquire() try { if (!this.itemsToFilter) { let itemsToFilter: T[] = [] const addNodes = async (nodes: ReadonlyArray): Promise => { for (let n of nodes) { itemsToFilter.push(n) let arr = await Promise.resolve(this.provider.getChildren(n)) if (!isFalsyOrEmpty(arr)) await addNodes(arr) } } let nodes = await Promise.resolve(this.provider.getChildren()) await addNodes(nodes) this.itemsToFilter = itemsToFilter } let lowInput = text.toLowerCase() let emptyInput = text.length === 0 for (let n of this.itemsToFilter) { let item = await this.getTreeItem(n) let label = getItemLabel(item) let score = 0 if (!emptyInput) { let res = fuzzyScoreGracefulAggressive(text, lowInput, 0, label, label.toLowerCase(), 0, { boostFullMatch: true, firstMatchCanBeWeak: true }) if (!res) continue score = res[0] item.label = { label, highlights: toSpans(label, res) } } else { item.label = { label, highlights: [] } } item.collapsibleState = TreeItemCollapsibleState.None let { line, highlights } = this.getRenderedLine(item, index, 0) items.push({ level: 0, node: n, line, index, score, highlights }) index += 1 } items.sort((a, b) => { if (a.score != b.score) return b.score - a.score return a.index - b.index }) let lnum = this.startLnum let highlights: HighlightItem[] = [] let renderedItems = this.renderedItems = items.map((o, idx) => { highlights.push(...o.highlights.map(h => { h.lnum = lnum + idx return h })) delete o.index delete o.score delete o.highlights return o }) this.updateUI(renderedItems.map(o => o.line), highlights, lnum, -1, true) if (renderedItems.length) { this.selectItem(renderedItems[0].node, true) } else { this.clearSelection() } this.redraw() release() } catch (e) { release() logger.error(`Error on tree filter:`, e) } } public async onHover(lnum: number): Promise { let element = this.getElementByLnum(lnum - 1) if (!element || !this.nodesMap.has(element)) return let obj = this.nodesMap.get(element) let item = obj.item if (!item.tooltip && !obj.resolved) item = await this.resolveItem(element, item) if (!item.tooltip) return let isMarkdown = MarkupContent.is(item.tooltip) && item.tooltip.kind == MarkupKind.Markdown let doc: Documentation = { filetype: isMarkdown ? 'markdown' : 'txt', content: MarkupContent.is(item.tooltip) ? item.tooltip.value : item.tooltip } await this.tooltipFactory.show([doc]) } private async onClick(element: T): Promise { let { nvim } = this let [line, col] = await nvim.eval(`[getline('.'),col('.')]`) as [string, number] let pre = byteSlice(line, 0, col - 1) let character = line[pre.length] let { openedIcon, closedIcon } = this.config if (/^\s*$/.test(pre) && [openedIcon, closedIcon].includes(character)) { await this.toggleExpand(element) } else { await this.invokeCommand(element) } } public async invokeCommand(element: T): Promise { let obj = this.nodesMap.get(element) if (!obj) return this.selectItem(element) let item = obj.item if (!item.command) item = await this.resolveItem(element, item) if (!item || !item.command) throw new Error(`Failed to resolve command from TreeItem.`) await commandManager.execute(item.command) } public async invokeActions(element: T | undefined): Promise { if (!element) return this.selectItem(element) if (typeof this.provider.resolveActions !== 'function') { await window.showWarningMessage('No actions') return } let obj = this.nodesMap.get(element) let actions = await Promise.resolve(this.provider.resolveActions(obj.item, element)) if (!actions || actions.length == 0) { await window.showWarningMessage('No actions available') return } let keys = actions.map(o => o.title) let res = await window.showMenuPicker(keys, 'Choose action') if (res == -1) return await Promise.resolve(actions[res].handler(element)) } private async onDataChange(node: T | undefined | null | void): Promise { if (this.filtering) { this.itemsToFilter = undefined await this.doFilter(toText(this.filterText)) return } this.clearSelection() if (!node) { await this.render() return } let release = await this.mutex.acquire() try { let items = this.renderedItems let idx = items.findIndex(o => o.node === node) if (idx != -1 && this.bufnr) { let obj = items[idx] let level = obj.level let removeCount = 0 for (let i = idx; i < items.length; i++) { let o = items[i] if (i == idx || o && o.level > level) { removeCount += 1 } } let appendItems: RenderedItem[] = [] let highlights: HighlightItem[] = [] let start = idx + this.startLnum await this.appendTreeNode(node, level, start, appendItems, highlights) items.splice(idx, removeCount, ...appendItems) this.updateUI(appendItems.map(o => o.line), highlights, start, start + removeCount) } release() } catch (e) { let errMsg = `Error on tree refresh: ${e}` logger.error(errMsg, e) this.nvim.errWriteLine('[coc.nvim] ' + errMsg) release() } } private async resolveItem(element: T, item: TreeItem): Promise { if (typeof this.provider.resolveTreeItem === 'function') { let tokenSource = this.resolveTokenSource = new CancellationTokenSource() let token = tokenSource.token item = await Promise.resolve(this.provider.resolveTreeItem(item, element, token)) tokenSource.dispose() this.resolveTokenSource = undefined if (token.isCancellationRequested) return undefined } this.nodesMap.set(element, { item, resolved: true }) return item } public get visible(): boolean { if (!this.bufnr) return false return this.winid != null } public get valid(): boolean { return typeof this.bufnr === 'number' } public get selection(): T[] { return this._selection.slice() } public async checkLines(): Promise { if (!this.bufnr) return false let buf = this.nvim.createBuffer(this.bufnr) let curr = await buf.lines let { titleCount, messageCount } = this.lineState curr = curr.slice(titleCount + messageCount) let lines = this.renderedItems.map(o => o.line) return equals(curr, lines) } /** * Expand/collapse TreeItem. */ private async toggleExpand(element: T | undefined): Promise { let o = this.nodesMap.get(element) if (!o) return let treeItem = o.item let lnum = this.getItemLnum(element) let nodeIdx = lnum - this.startLnum let obj = this.renderedItems[nodeIdx] if (!obj || treeItem.collapsibleState == TreeItemCollapsibleState.None) { if (typeof this.provider.getParent === 'function') { let node = await Promise.resolve(this.provider.getParent(element)) if (node) { await this.toggleExpand(node) this.focusItem(node) } } return } // remove lines let removeCount = 0 if (treeItem.collapsibleState == TreeItemCollapsibleState.Expanded) { let level = obj.level for (let i = nodeIdx + 1; i < this.renderedItems.length; i++) { let o = this.renderedItems[i] if (!o || o.level <= level) break removeCount += 1 } treeItem.collapsibleState = TreeItemCollapsibleState.Collapsed } else if (treeItem.collapsibleState == TreeItemCollapsibleState.Collapsed) { treeItem.collapsibleState = TreeItemCollapsibleState.Expanded } let newItems: RenderedItem[] = [] let newHighlights: HighlightItem[] = [] await this.appendTreeNode(obj.node, obj.level, lnum, newItems, newHighlights) this.renderedItems.splice(nodeIdx, removeCount + 1, ...newItems) this.updateUI(newItems.map(o => o.line), newHighlights, lnum, lnum + removeCount + 1) this.refreshSigns() if (treeItem.collapsibleState == TreeItemCollapsibleState.Collapsed) { this._onDidCollapseElement.fire({ element }) } else { this._onDidExpandElement.fire({ element }) } } private toggleSelection(element: T | undefined): void { if (!element) return let idx = this._selection.findIndex(o => o === element) if (idx !== -1) { this.unselectItem(idx) } else { this.selectItem(element) } } private clearSelection(): void { if (!this.bufnr) return this._selection = [] let buf = this.nvim.createBuffer(this.bufnr) buf.unplaceSign({ group: 'CocTree' }) this._onDidChangeSelection.fire({ selection: [] }) } public selectItem(item: T, forceSingle?: boolean, noRedraw?: boolean): void { let { nvim } = this let row = this.getItemLnum(item) if (row == null || !this.bufnr) return let buf = nvim.createBuffer(this.bufnr) let exists = this._selection.includes(item) if (!this.opts.canSelectMany || forceSingle) { this._selection = [item] } else if (!exists) { this._selection.push(item) } nvim.pauseNotification() if (!this.opts.canSelectMany || forceSingle) { buf.unplaceSign({ group: 'CocTree' }) } nvim.call('win_execute', [this.winid, `normal! ${row + 1}G`], true) buf.placeSign({ id: signOffset + row, lnum: row + 1, name: 'CocTreeSelected', group: 'CocTree' }) if (!noRedraw) this.redraw() nvim.resumeNotification(false, true) if (!exists) this._onDidChangeSelection.fire({ selection: this._selection }) } public unselectItem(idx: number): void { let item = this._selection[idx] let row = this.getItemLnum(item) if (row == null || !this.bufnr) return this._selection.splice(idx, 1) let buf = this.nvim.createBuffer(this.bufnr) buf.unplaceSign({ group: 'CocTree', id: signOffset + row }) this._onDidChangeSelection.fire({ selection: this._selection }) } public focusItem(element: T): void { if (!this.winid) return let lnum = this.getItemLnum(element) if (lnum == null) return this.nvim.call('win_execute', [this.winid, `exe ${lnum + 1}`], true) } private getElementByLnum(lnum: number): T | undefined { let item = this.renderedItems[lnum - this.startLnum] return item ? item.node : undefined } private getItemLnum(item: T): number | undefined { let idx = this.renderedItems.findIndex(o => o.node === item) if (idx == -1) return undefined return this.startLnum + idx } private async getTreeItem(element: T): Promise { let exists: TreeItem let resolved = false let obj = this.nodesMap.get(element) if (obj != null) { exists = obj.item resolved = obj.resolved } let item = await Promise.resolve(this.provider.getTreeItem(element)) if (exists && item && exists.collapsibleState != TreeItemCollapsibleState.None && item.collapsibleState != TreeItemCollapsibleState.None) { item.collapsibleState = exists.collapsibleState } this.nodesMap.set(element, { item, resolved }) return item } private getRenderedLine(treeItem: TreeItem, lnum: number, level: number): { line: string, highlights: HighlightItem[] } { let { openedIcon, closedIcon } = this.config const highlights: HighlightItem[] = [] const { label, deprecated, description } = treeItem let prefix = ' '.repeat(level) const addHighlight = (text: string, hlGroup: string) => { let colStart = byteLength(prefix) highlights.push({ lnum, hlGroup, colStart, colEnd: colStart + byteLength(text), }) } switch (treeItem.collapsibleState) { case TreeItemCollapsibleState.Expanded: { addHighlight(openedIcon, 'CocTreeOpenClose') prefix += openedIcon + ' ' break } case TreeItemCollapsibleState.Collapsed: { addHighlight(closedIcon, 'CocTreeOpenClose') prefix += closedIcon + ' ' break } default: prefix += this.leafIndent ? ' ' : '' } if (treeItem.icon) { let { text, hlGroup } = treeItem.icon addHighlight(text, hlGroup) prefix += text + ' ' } if (TreeItemLabel.is(label) && Array.isArray(label.highlights)) { let colStart = byteLength(prefix) for (let o of label.highlights) { highlights.push({ lnum, hlGroup: 'CocSearch', colStart: colStart + o[0], colEnd: colStart + o[1] }) } } let labelText = getItemLabel(treeItem) if (deprecated) { addHighlight(labelText, 'CocDeprecatedHighlight') } prefix += labelText if (description && description.indexOf('\n') == -1) { prefix += ' ' addHighlight(description, 'CocTreeDescription') prefix += description } return { line: prefix, highlights } } private async appendTreeNode(element: T, level: number, lnum: number, items: RenderedItem[], highlights: HighlightItem[]): Promise { let treeItem = await this.getTreeItem(element) if (!treeItem) return 0 let takes = 1 let res = this.getRenderedLine(treeItem, lnum, level) highlights.push(...res.highlights) items.push({ level, line: res.line, node: element }) if (treeItem.collapsibleState == TreeItemCollapsibleState.Expanded) { let l = level + 1 let children = await Promise.resolve(this.provider.getChildren(element)) for (let el of toArray(children as any)) { let n = await this.appendTreeNode(el, l, lnum + takes, items, highlights) takes = takes + n } } return takes } private updateUI(lines: string[], highlights: HighlightItem[], start = 0, end = -1, noRedraw = false): void { if (!this.bufnr) return let { nvim, winid } = this let buf = nvim.createBuffer(this.bufnr) nvim.pauseNotification() buf.setOption('modifiable', true, true) void buf.setLines(lines, { start, end, strictIndexing: false }, true) if (this.autoWidth) this.nvim.call('coc#window#adjust_width', [winid], true) if (highlights.length) { let highlightEnd = end == -1 ? -1 : start + lines.length buf.updateHighlights(highlightNamespace, highlights, { start, end: highlightEnd }) } buf.setOption('modifiable', false, true) if (!noRedraw) this.redraw() nvim.resumeNotification(false, true) } public async reveal(element: T, options: { select?: boolean; focus?: boolean; expand?: number | boolean } = {}): Promise { if (this.filtering) return let isShown = this.getItemLnum(element) != null let { select, focus, expand } = options let curr = element if (typeof this.provider.getParent !== 'function') { throw new Error('missing getParent function from provider for reveal.') } if (!isShown) { while (curr) { let parentNode = await Promise.resolve(this.provider.getParent(curr)) if (parentNode) { let item = await this.getTreeItem(parentNode) item.collapsibleState = TreeItemCollapsibleState.Expanded curr = parentNode } else { break } } } if (expand) { let item = await this.getTreeItem(element) // Unable to expand if (item.collapsibleState != TreeItemCollapsibleState.None) { item.collapsibleState = TreeItemCollapsibleState.Expanded if (typeof expand === 'boolean') expand = 1 if (expand > 1) { let curr = Math.min(expand, 2) let nodes = await Promise.resolve(this.provider.getChildren(element)) while (!isFalsyOrEmpty(nodes)) { let arr: T[] = [] for (let n of nodes) { let item = await this.getTreeItem(n) if (item.collapsibleState == TreeItemCollapsibleState.None) continue item.collapsibleState = TreeItemCollapsibleState.Expanded if (curr > 1) { let res = await Promise.resolve(this.provider.getChildren(n)) arr.push(...res) } } nodes = arr curr = curr - 1 } } } } if (!isShown || expand) { await this.render() } if (select !== false) this.selectItem(element) if (focus) this.focusItem(element) } private updateHeadLines(initialize = false): void { let { titleCount, messageCount } = this.lineState let end = initialize ? -1 : titleCount + messageCount let lines: string[] = [] let highlights: HighlightItem[] = [] if (this.message) { highlights.push({ hlGroup: 'MoreMsg', colStart: 0, colEnd: byteLength(this.message), lnum: 0 }) lines.push(this.message) lines.push('') } if (this.title) { highlights.push({ hlGroup: 'CocTreeTitle', colStart: 0, colEnd: byteLength(this.title), lnum: lines.length }) if (this.description) { let colStart = byteLength(this.title) + 1 highlights.push({ hlGroup: 'Comment', colStart, colEnd: colStart + byteLength(this.description), lnum: lines.length }) } lines.push(this.title + (this.description ? ' ' + this.description : '')) } this.lineState.messageCount = this.message ? 2 : 0 this.lineState.titleCount = this.title ? 1 : 0 this.updateUI(lines, highlights, 0, end) if (!initialize) { this.refreshSigns() } } /** * Update signs after collapse/expand or head change */ private refreshSigns(): void { let { selection, nvim, bufnr } = this if (!selection.length || !bufnr) return let buf = nvim.createBuffer(bufnr) nvim.pauseNotification() buf.unplaceSign({ group: 'CocTree' }) for (let n of selection) { let row = this.getItemLnum(n) if (row == null) continue buf.placeSign({ id: signOffset + row, lnum: row + 1, name: 'CocTreeSelected', group: 'CocTree' }) } nvim.resumeNotification(false, true) } // Render all tree items public async render(): Promise { if (!this.bufnr) return let release = await this.mutex.acquire() try { let lines: string[] = [] let highlights: HighlightItem[] = [] let { startLnum } = this let nodes = await Promise.resolve(this.provider.getChildren()) let level = 0 let lnum = startLnum let renderedItems: RenderedItem[] = [] if (isFalsyOrEmpty(nodes)) { this.message = 'No results' } else { if (this.message == 'No results') this.message = '' for (let node of nodes) { let n = await this.appendTreeNode(node, level, lnum, renderedItems, highlights) lnum += n } } lines.push(...renderedItems.map(o => o.line)) this.renderedItems = renderedItems let delta = this.startLnum - startLnum highlights.forEach(o => o.lnum = o.lnum + delta) this.updateUI(lines, highlights, this.startLnum, -1) this._onDidRefrash.fire() this.retryTimers = 0 release() } catch (err) { logger.error('Error on render', err) this.renderedItems = [] this.nodesMap.clear() this.lineState = { titleCount: 0, messageCount: 1 } release() let errMsg = `${err}`.replace(/\r?\n/g, ' ') this.updateUI([errMsg], [{ hlGroup: 'WarningMsg', colStart: 0, colEnd: byteLength(errMsg), lnum: 0 }]) if (this.retryTimers == maxRetry) return this.timer = setTimeout(() => { this.retryTimers = this.retryTimers + 1 void this.render() }, retryTimeout) } } public async show(splitCommand = 'belowright 30vs', waitRender = true): Promise { let { nvim } = this let [targetBufnr, windowId] = await nvim.eval(`[bufnr("%"),win_getid()]`) as [number, number] this._targetBufnr = targetBufnr this._targetWinId = windowId let opts = { command: splitCommand, bufname: this.bufname, viewId: this.viewId.replace(/"/g, '\\"'), bufnr: defaultValue(this.bufnr, -1), winid: defaultValue(this.winid, -1), bufhidden: defaultValue(this.opts.bufhidden, 'wipe'), canSelectMany: this.opts.canSelectMany === true, winfixwidth: this.winfixwidth === true } let [bufnr, winid, tabId] = await nvim.call('coc#ui#create_tree', [opts]) as [number, number, number] this.bufnr = bufnr this.winid = winid this._targetTabId = tabId if (winid != opts.winid) this._onDidChangeVisibility.fire({ visible: true }) if (bufnr == opts.bufnr) return true this.registerKeymaps() this.updateHeadLines(true) let promise = this.render() if (waitRender) await promise return true } public registerLocalKeymap(mode: LocalMode, key: string, fn: (element: T | undefined) => Promise | void, notify = false): void { if (!this.bufnr) { this._keymapDefs.push({ mode, key, fn, notify }) } else { this.addLocalKeymap(mode, key, fn, notify) } } private addLocalKeymap(mode: LocalMode, key: string | undefined, fn: (element: T | undefined) => Promise | void, notify = true): void { if (!key) return workspace.registerLocalKeymap(this.bufnr, mode, key, async () => { let lnum = await this.nvim.call('line', ['.']) as number let element = this.getElementByLnum(lnum - 1) await Promise.resolve(fn(element)) }, notify) } private registerKeymaps(): void { let { toggleSelection, actions, close, invoke, toggle, collapseAll, activeFilter } = this.keys let { nvim, _keymapDefs } = this this.disposables.push(workspace.registerLocalKeymap(this.bufnr, 'n', '', () => { nvim.call('win_gotoid', [this._targetWinId], true) }, true)) this.addLocalKeymap('n', '', async element => { if (element) await this.onClick(element) }) if (this.filter != null) { this.addLocalKeymap('n', activeFilter, async () => { this.nvim.command(`exe ${this.startLnum}`, true) this.filter.active() this.filterText = '' this._onDidFilterStateChange.fire(true) }) } this.addLocalKeymap('n', toggleSelection, element => this.toggleSelection(element)) this.addLocalKeymap('n', invoke, element => this.invokeCommand(element)) this.addLocalKeymap('n', actions, element => this.invokeActions(element)) this.addLocalKeymap('n', toggle, element => this.toggleExpand(element)) this.addLocalKeymap('n', collapseAll, () => this.collapseAll()) this.addLocalKeymap('n', close, () => this.hide()) while (_keymapDefs.length) { const def = _keymapDefs.pop() this.addLocalKeymap(def.mode, def.key, def.fn, def.notify) } } public hide(): void { this.nvim.call('coc#window#close', [this.winid], true) this.redraw() this.winid = undefined this._onDidChangeVisibility.fire({ visible: false }) } private redraw(): void { if (workspace.isVim || this.filter?.activated) { this.nvim.command('redraw', true) } } private async collapseAll(): Promise { for (let obj of this.nodesMap.values()) { let item = obj.item if (item.collapsibleState == TreeItemCollapsibleState.Expanded) { item.collapsibleState = TreeItemCollapsibleState.Collapsed } } await this.render() } private cancelResolve(): void { if (this.resolveTokenSource) { this.resolveTokenSource.cancel() this.resolveTokenSource = undefined } } public dispose(): void { if (!this.provider) return if (this.timer) clearTimeout(this.timer) this.cancelResolve() let { bufnr } = this if (this.winid) this._onDidChangeVisibility.fire({ visible: false }) if (bufnr) this.nvim.command(`silent! bwipeout! ${bufnr}`, true) this._keymapDefs = [] this.winid = undefined this.bufnr = undefined this.filter?.dispose() this._selection = [] this.itemsToFilter = [] this.tooltipFactory.dispose() this.renderedItems = [] this.nodesMap.clear() this.provider = undefined this._onDispose.fire() this._onDispose.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/tree/filter.ts ================================================ 'use strict' import events from '../events' import { Neovim } from '@chemzqm/neovim' import { Disposable, Emitter, Event } from '../util/protocol' import { disposeAll } from '../util' export const sessionKey = 'filter' export class HistoryInput { private history: string[] = [] public next(input: string): string | undefined { let idx = this.history.indexOf(input) return this.history[idx + 1] ?? this.history[0] } public previous(input: string): string | undefined { let idx = this.history.indexOf(input) return this.history[idx - 1] ?? this.history[this.history.length - 1] } public add(input: string): void { let idx = this.history.indexOf(input) if (idx !== -1) { this.history.splice(idx, 1) } this.history.unshift(input) } public toJSON(): string { return `[${this.history.join(',')}]` } } export default class Filter { private _activated = false private text: string private history = new HistoryInput() private disposables: Disposable[] = [] private readonly _onDidUpdate = new Emitter() private readonly _onDidExit = new Emitter() private readonly _onDidKeyPress = new Emitter() public readonly onDidKeyPress: Event = this._onDidKeyPress.event public readonly onDidUpdate: Event = this._onDidUpdate.event public readonly onDidExit: Event = this._onDidExit.event constructor(private nvim: Neovim, keys: string[]) { this.text = '' events.on('InputChar', (session, character) => { if (session !== sessionKey || !this._activated) return if (!keys.includes(character)) { if (character.length == 1) { this.text = this.text + character this._onDidUpdate.fire(this.text) return } if (character == '' || character == '') { this.text = this.text.slice(0, -1) this._onDidUpdate.fire(this.text) return } if (character == '') { this.text = '' this._onDidUpdate.fire(this.text) return } if (character == '') { let text = this.history.next(this.text) if (text) { this.text = text this._onDidUpdate.fire(this.text) } return } if (character == '') { let text = this.history.previous(this.text) if (text) { this.text = text this._onDidUpdate.fire(this.text) } } if (character == '' || character == '') { this.deactivate() return } } this._onDidKeyPress.fire(character) }, null, this.disposables) } public active(): void { this._activated = true this.text = '' this.nvim.call('coc#prompt#start_prompt', [sessionKey], true) } public deactivate(node?: T): void { if (!this._activated) return this.nvim.call('coc#prompt#stop_prompt', [sessionKey], true) this._activated = false let { text } = this this.text = '' this._onDidExit.fire(node) this.history.add(text) } public get activated(): boolean { return this._activated } public dispose(): void { this.deactivate() this._onDidKeyPress.dispose() this._onDidUpdate.dispose() this._onDidExit.dispose() disposeAll(this.disposables) } } ================================================ FILE: src/tree/index.ts ================================================ 'use strict' import type { Disposable, Event, CancellationToken } from '../util/protocol' import type { ProviderResult } from '../provider' import { TreeItem, TreeItemCollapsibleState } from './TreeItem' export { TreeItem, TreeItemCollapsibleState } export interface TreeItemAction { /** * Label text in menu. */ title: string handler: (item: T) => ProviderResult } export interface LineState { /** * Line count used by message */ messageCount: number /** * Line count used by title */ titleCount: number } export interface TreeViewKeys { invoke: string toggle: string actions: string collapseAll: string toggleSelection: string close: string activeFilter: string selectNext: string selectPrevious: string } export interface TreeItemData { item: TreeItem resolved: boolean } /** * Options for creating a {@link TreeView} */ export interface TreeViewOptions { /** * bufhidden option for TreeView, default to 'wipe' */ bufhidden?: 'hide' | 'unload' | 'delete' | 'wipe' /** * Increase width to avoid wrapped lines. */ autoWidth?: boolean /** * Fixed width for window, default to true */ winfixwidth?: boolean /** * Enable filter feature, default to false */ enableFilter?: boolean /** * Disable indent of leaves without children, default to false */ disableLeafIndent?: boolean /** * A data provider that provides tree data. */ treeDataProvider: TreeDataProvider /** * Whether the tree supports multi-select. When the tree supports multi-select and a command is executed from the tree, * the first argument to the command is the tree item that the command was executed on and the second argument is an * array containing all selected tree items. */ canSelectMany?: boolean } /** * The event that is fired when an element in the {@link TreeView} is expanded or collapsed */ export interface TreeViewExpansionEvent { /** * Element that is expanded or collapsed. */ readonly element: T } /** * The event that is fired when there is a change in {@link TreeView.selection tree view's selection} */ export interface TreeViewSelectionChangeEvent { readonly selection: T[] } /** * The event that is fired when there is a change in {@link TreeView.visible tree view's visibility} */ export interface TreeViewVisibilityChangeEvent { readonly visible: boolean } /** * Represents a Tree view */ export interface TreeView extends Disposable { /** * Event that is fired when an element is expanded */ readonly onDidExpandElement: Event> /** * Event that is fired when an element is collapsed */ readonly onDidCollapseElement: Event> /** * Currently selected elements. */ readonly selection: T[] /** * Event that is fired when the {@link TreeView.selection selection} has changed */ readonly onDidChangeSelection: Event> /** * `true` if the {@link TreeView tree view} is visible otherwise `false`. * * **NOTE:** is `true` when TreeView visible on other tab. */ readonly visible: boolean /** * Window id used by TreeView. */ readonly windowId: number | undefined /** * Event that is fired when {@link TreeView.visible visibility} has changed */ readonly onDidChangeVisibility: Event /** * An optional human-readable message that will be rendered in the view. * Setting the message to null, undefined, or empty string will remove the message from the view. */ message?: string /** * The tree view title is initially taken from viewId of TreeView * Changes to the title property will be properly reflected in the UI in the title of the view. */ title?: string /** * An optional human-readable description which is rendered less prominently in the title of the view. * Setting the title description to null, undefined, or empty string will remove the description from the view. */ description?: string /** * Reveals the given element in the tree view. * If the tree view is not visible then the tree view is shown and element is revealed. * * By default revealed element is selected. * In order to not to select, set the option `select` to `false`. * In order to focus, set the option `focus` to `true`. * In order to expand the revealed element, set the option `expand` to `true`. To expand recursively set `expand` to the number of levels to expand. * **NOTE:** You can expand only to 3 levels maximum. * * **NOTE:** The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ reveal(element: T, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable /** * Create tree view in new window, does nothing when it's visible. * * **NOTE:** * **NOTE:** TreeView with same viewId in current tab would be disposed. * @param splitCommand The command to open TreeView window, default to 'belowright 30vs' */ show(splitCommand?: string): Promise } /** * A data provider that provides tree data */ export interface TreeDataProvider { /** * An optional event to signal that an element or root has changed. * This will trigger the view to update the changed element/root and its children recursively (if shown). * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. */ onDidChangeTreeData?: Event /** * Get {@link TreeItem} representation of the `element` * @param element The element for which {@link TreeItem} representation is asked for. * @return {@link TreeItem} representation of the element */ getTreeItem(element: T): TreeItem | Thenable /** * Get the children of `element` or root if no element is passed. * @param element The element from which the provider gets children. Can be `undefined`. * @return Children of `element` or root if no element is passed. */ getChildren(element?: T): ProviderResult> /** * Optional method to return the parent of `element`. * Return `null` or `undefined` if `element` is a child of root. * * **NOTE:** This method should be implemented in order to access {@link TreeView.reveal reveal} API. * @param element The element for which the parent has to be returned. * @return Parent of `element`. */ getParent?(element: T): ProviderResult /** * Called on hover to resolve the {@link TreeItem.tooltip TreeItem} property if it is undefined. * Called on tree item click/open to resolve the {@link TreeItem.command TreeItem} property if it is undefined. * Only properties that were undefined can be resolved in `resolveTreeItem`. * Functionality may be expanded later to include being called to resolve other missing * properties on selection and/or on open. * * Will only ever be called once per TreeItem. * * onDidChangeTreeData should not be triggered from within resolveTreeItem. * * *Note* that this function is called when tree items are already showing in the UI. * Because of that, no property that changes the presentation (label, description, etc.) * can be changed. * @param item Undefined properties of `item` should be set then `item` should be returned. * @param element The object associated with the TreeItem. * @param token A cancellation token. * @return The resolved tree item or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ resolveTreeItem?(item: TreeItem, element: T, token: CancellationToken): ProviderResult /** * Called with current element to resolve actions. * Called when user press 'actions' key. * @param item Resolved item. * @param element The object under cursor. */ resolveActions?(item: TreeItem, element: T): ProviderResult>> } ================================================ FILE: src/types.ts ================================================ 'use strict' import type { Window } from '@chemzqm/neovim' import type { Disposable, Event } from 'vscode-languageserver-protocol' import type { CreateFile, DeleteFile, Diagnostic, Location, Position, Range, RenameFile, TextDocumentEdit } from 'vscode-languageserver-types' import type { URI } from 'vscode-uri' import type RelativePattern from './model/relativePattern' import type { LinesTextDocument } from './model/textdocument' export type { IConfigurationChangeEvent } from './configuration/types' export type GlobPattern = string | RelativePattern declare global { namespace NodeJS { interface Global { __isMain?: boolean __TEST__?: boolean __starttime?: number REVISION?: string WebAssembly: any } } } export type HoverTarget = 'float' | 'preview' | 'echo' export type Optional = Omit< T, K > & Partial> export interface Thenable { then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable // eslint-disable-next-line @typescript-eslint/unified-signatures then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable } export interface AnsiHighlight { span: [number, number] hlGroup: string } export interface FileWatchConfig { readonly watchmanPath: string | null | undefined readonly enable: boolean readonly ignoredFolders: string[] } export interface LocationWithTarget extends Location { /** * The full target range of this link. If the target for example is a symbol then target range is the * range enclosing this symbol not including leading/trailing whitespace but everything else like comments. This information is typically used to highlight the range in the editor. */ targetRange?: Range } export interface BufferOption { readonly bufnr: number readonly eol: number readonly size: number readonly winid: number readonly winids: number[] readonly lines: null | string[] readonly variables: { [key: string]: any } readonly bufname: string readonly commandline: number readonly fullpath: string readonly buftype: string readonly filetype: string readonly iskeyword: string readonly lisp: number readonly changedtick: number readonly previewwindow: number } export interface IFileSystemWatcher extends Disposable { ignoreCreateEvents: boolean ignoreChangeEvents: boolean ignoreDeleteEvents: boolean onDidCreate: Event onDidChange: Event onDidDelete: Event } export interface Documentation { filetype: string content: string highlights?: HighlightItem[] active?: [number, number] } export interface FloatFactory { window: Window | null activated: () => Promise show: (docs: Documentation[], options?: FloatOptions) => Promise close: () => void checkRetrigger: (bufnr: number) => boolean dispose: () => void } export interface FloatConfig { border?: boolean | [number, number, number, number] rounded?: boolean highlight?: string title?: string borderhighlight?: string close?: boolean maxHeight?: number maxWidth?: number winblend?: number focusable?: boolean shadow?: boolean } export interface FloatOptions extends FloatConfig { title?: string offsetX?: number } export interface HighlightItemOption { /** * default to true */ combine?: boolean /** * default to false */ start_incl?: boolean /** * default to false */ end_incl?: boolean } /** * Represent a highlight that not cross lines * all zero based. */ export interface HighlightItem extends HighlightItemOption { lnum: number hlGroup: string /** * 0 based start column. */ colStart: number /** * 0 based end column. */ colEnd: number } export interface Env { runtimepath: string readonly jumpAutocmd: boolean readonly guicursor: string readonly tabCount: number readonly mode: string readonly apiversion: number readonly pumwidth: number readonly unixPrefix: string readonly ambiguousIsNarrow: boolean readonly floating: boolean readonly sign: boolean readonly globalExtensions: string[] readonly workspaceFolders: string[] readonly config: any readonly pid: number readonly columns: number readonly lines: number readonly pumevent: boolean readonly cmdheight: number readonly filetypeMap: { [index: string]: string } readonly isVim: boolean readonly isCygwin: boolean readonly isMacvim: boolean readonly isiTerm: boolean readonly version: string readonly locationlist: boolean readonly progpath: string readonly dialog: boolean readonly terminal: boolean readonly textprop: boolean readonly vimCommands: CommandConfig[] readonly semanticHighlights: string[] } export interface CommandConfig { id: string cmd: string title?: string } /** * An output channel is a container for readonly textual information. * * To get an instance of an `OutputChannel` use * [createOutputChannel](#window.createOutputChannel). */ export interface OutputChannel { /** * The human-readable name of this output channel. */ readonly name: string readonly content?: string /** * Append the given value to the channel. * @param value A string, falsy values will not be printed. */ append(value: string): void /** * Append the given value and a line feed character * to the channel. * @param value A string, falsy values will be printed. */ appendLine(value: string): void /** * Removes output from the channel. Latest `keep` lines will be remained. */ clear(keep?: number): void /** * Reveal this channel in the UI. * @param preserveFocus When `true` the channel will not take focus. */ show(preserveFocus?: boolean): void /** * Hide this channel from the UI. */ hide(): void /** * Dispose and free associated resources. */ dispose(): void } export interface KeymapOption { /** * Use `` prefix */ cmd?: boolean sync?: boolean cancel?: boolean silent?: boolean repeat?: boolean special?: boolean } export interface TabStopInfo { // tabstop index index: number // 0 based line character range: [number, number, number, number] // current text text: string } export interface JumpInfo { readonly index: number readonly forward: boolean readonly tabstops: TabStopInfo[] // placeholder range readonly range: Range // character before current placeholder. readonly charbefore: string readonly snippet_start: Position readonly snippet_end: Position } export interface Autocmd { event: string | string[] callback: (...args: any[]) => void | Promise buffer?: number once?: boolean nested?: boolean pattern?: string | string[] arglist?: string[] request?: boolean thisArg?: any } export interface UltiSnipsActions { preExpand?: string postExpand?: string postJump?: string } export interface UltiSnippetOption { regex?: string context?: string noPython?: boolean range?: Range line?: string actions?: UltiSnipsActions /** * Do not expand tabs */ noExpand?: boolean /** * Trim all whitespaces from right side of snippet lines. */ trimTrailingWhitespace?: boolean /** * Remove whitespace immediately before the cursor at the end of a line before jumping to the next tabstop */ removeWhiteSpace?: boolean } export interface TextDocumentMatch { readonly uri: string readonly languageId: string } export interface QuickfixItem { uri?: string module?: string range?: Range text?: string type?: string filename: string bufnr?: number lnum?: number end_lnum?: number col?: number end_col?: number valid?: boolean nr?: number targetRange?: Range } /** * Represents an item that can be selected from * a list of items. */ export interface QuickPickItem { /** * A human-readable string which is rendered prominent */ label: string /** * A human-readable string which is rendered less prominent in the same line */ description?: string /** * Optional flag indicating if this item is picked initially. */ picked?: boolean } // TextDocument {{ export type DocumentChange = TextDocumentEdit | CreateFile | RenameFile | DeleteFile /** * An event describing a change to a text document. */ export interface TextDocumentContentChange { /** * The range of the document that changed. */ range: Range /** * The optional length of the range that got replaced. * @deprecated use range instead. */ rangeLength?: number /** * The new text for the provided range. */ text: string } export interface DidChangeTextDocumentParams { /** * The document that did change. The version number points * to the version after all provided content changes have * been applied. */ readonly textDocument: { version: number uri: string } readonly document: LinesTextDocument /** * The actual content changes. The content changes describe single state changes * to the document. So if there are two content changes c1 (at array index 0) and * c2 (at array index 1) for a document in state S then c1 moves the document from * S to S' and c2 from S' to S''. So c1 is computed on the state S and c2 is computed * on the state S'. */ readonly contentChanges: ReadonlyArray /** * Buffer number of document. */ readonly bufnr: number /** * Original content before change */ readonly original: string /** * Changed lines */ readonly originalLines: ReadonlyArray } // }} export interface DiagnosticWithFileType extends Diagnostic { /** * The `filetype` property provides the type of file associated with the diagnostic information. * This information is utilized by the diagnostic buffer panel for highlighting and formatting * the diagnostic messages according to the specific filetype. */ filetype?: string } ================================================ FILE: src/util/ansiparse.ts ================================================ 'use strict' import { byteLength, toText, upperFirst } from './string' export interface AnsiItem { foreground?: string background?: string bold?: boolean italic?: boolean underline?: boolean strikethrough?: boolean text: string } const foregroundColors = { 30: 'black', 31: 'red', 32: 'green', 33: 'yellow', 34: 'blue', 35: 'magenta', 36: 'cyan', 37: 'white', 90: 'grey' } const backgroundColors = { 40: 'black', 41: 'red', 42: 'green', 43: 'yellow', 44: 'blue', 45: 'magenta', 46: 'cyan', 47: 'white' } const styles = { 1: 'bold', 3: 'italic', 4: 'underline', 9: 'strikethrough' } export interface AnsiHighlight { span: [number, number] hlGroup: string } export interface AnsiResult { line: string highlights: AnsiHighlight[] } export function parseAnsiHighlights(line: string, markdown = false): AnsiResult { let items = ansiparse(line) let highlights: AnsiHighlight[] = [] let newLabel = '' for (let item of items) { if (!item.text) continue let { foreground, background } = item let len = byteLength(newLabel) let span: [number, number] = [len, len + byteLength(item.text)] if (foreground && background) { let hlGroup = `CocList${upperFirst(foreground)}${upperFirst(background)}` highlights.push({ span, hlGroup }) } else if (foreground) { let hlGroup: string if (markdown) { if (foreground == 'yellow') { hlGroup = 'CocMarkdownCode' } else if (foreground == 'blue') { hlGroup = 'CocMarkdownLink' } else if (foreground == 'magenta') { hlGroup = 'CocMarkdownHeader' } else { hlGroup = `CocListFg${upperFirst(foreground)}` } } else { hlGroup = `CocListFg${upperFirst(foreground)}` } highlights.push({ span, hlGroup }) } else if (background) { let hlGroup = `CocListBg${upperFirst(background)}` highlights.push({ span, hlGroup }) } if (item.bold) { highlights.push({ span, hlGroup: 'CocBold' }) } else if (item.italic) { highlights.push({ span, hlGroup: 'CocItalic' }) } else if (item.underline) { highlights.push({ span, hlGroup: 'CocUnderline' }) } else if (item.strikethrough) { highlights.push({ span, hlGroup: 'CocStrikeThrough' }) } newLabel = newLabel + item.text } return { line: newLabel, highlights } } export function ansiparse(str: string): AnsiItem[] { // // I'm terrible at writing parsers. // let matchingControl = null let matchingData = null let matchingText = '' let ansiState = [] let result = [] let state: Partial = {} let eraseChar // // General workflow for this thing is: // \033\[33mText // | | | // | | matchingText // | matchingData // matchingControl // // \033\[K or \033\[m // // In further steps we hope it's all going to be fine. It usually is. // // // Erases a char from the output // eraseChar = () => { let index let text if (matchingText.length) { matchingText = matchingText.substr(0, matchingText.length - 1) } else if (result.length) { index = result.length - 1 text = result[index].text if (text.length === 1) { // // A result bit was fully deleted, pop it out to simplify the final output // result.pop() } else { result[index].text = text.substr(0, text.length - 1) } } } for (let i = 0; i < str.length; i++) { if (matchingControl != null) { if (matchingControl == '\x1b' && str[i] == '[') { // // We've matched full control code. Lets start matching formatting data. // // // "emit" matched text with correct state // if (matchingText) { state.text = matchingText result.push(state) state = {} matchingText = '' } if (matchingText == '' && (str[i + 1] == 'm' || str[i + 1] == 'K')) { if (state.foreground || state.background) { state.text = '' result.push(state) } state = {} } matchingControl = null matchingData = '' } else { // // We failed to match anything - most likely a bad control code. We // go back to matching regular strings. // matchingText += matchingControl + str[i] matchingControl = null } continue } else if (matchingData != null) { if (str[i] == ';') { // // `;` separates many formatting codes, for example: `\033[33;43m` // means that both `33` and `43` should be applied. // // TODO: this can be simplified by modifying state here. // ansiState.push(matchingData) matchingData = '' } else if (str[i] == 'm' || str[i] == 'K') { // // `m` finished whole formatting code. We can proceed to matching // formatted text. // ansiState.push(matchingData) matchingData = null matchingText = '' // // Convert matched formatting data into user-friendly state object. // ansiState.forEach(ansiCode => { if (foregroundColors[ansiCode]) { state.foreground = foregroundColors[ansiCode] } else if (backgroundColors[ansiCode]) { state.background = backgroundColors[ansiCode] } else if (ansiCode == 39) { delete state.foreground } else if (ansiCode == 49) { delete state.background } else if (styles[ansiCode]) { state[styles[ansiCode]] = true } else if (ansiCode == 22) { state.bold = false } else if (ansiCode == 23) { state.italic = false } else if (ansiCode == 24) { state.underline = false } else if (ansiCode == 29) { state.strikethrough = false } }) ansiState = [] } else { matchingData += str[i] } continue } if (str[i] == '\x1b') { matchingControl = str[i] } else if (str[i] == '\u0008') { eraseChar() } else { matchingText += str[i] } } if (matchingText) { state.text = matchingText + toText(matchingControl) result.push(state) } return result } export function stripAnsiColoring(str?: string): string { // eslint-disable-next-line const ansiColorCodeRegex = /\u001b\[[0-9;]*m/g return str.replace(ansiColorCodeRegex, '') } ================================================ FILE: src/util/array.ts ================================================ 'use strict' export function toArray(item: T | T[] | null | undefined): T[] { return Array.isArray(item) ? item : item == null ? [] : [item] } /** * @returns false if the provided object is an array and not empty. */ export function isFalsyOrEmpty(obj: any): boolean { return !Array.isArray(obj) || obj.length === 0 } function compareValue(n: number, r: [number, number]): number { if (n < r[0]) return 1 if (n > r[1]) return -1 return 0 } /** * Check if n in sorted table */ export function intable(n: number, table: ReadonlyArray<[number, number]>): boolean { // do binary search let low = 0 let high = table.length - 1 while (low <= high) { const mid = ((low + high) / 2) | 0 const comp = compareValue(n, table[mid]) if (comp < 0) { low = mid + 1 } else if (comp > 0) { high = mid - 1 } else { return true } } return false } /** * Performs a binary search algorithm over a sorted array. * @param array The array being searched. * @param key The value we search for. * @param comparator A function that takes two array elements and returns zero * if they are equal, a negative number if the first element precedes the * second one in the sorting order, or a positive number if the second element * precedes the first one. * @return See {@link binarySearch2} */ export function binarySearch(array: ReadonlyArray, key: T, comparator: (op1: T, op2: T) => number): number { return binarySearch2(array.length, i => comparator(array[i], key)) } /** * Performs a binary search algorithm over a sorted collection. Useful for cases * when we need to perform a binary search over something that isn't actually an * array, and converting data to an array would defeat the use of binary search * in the first place. * @param length The collection length. * @param compareToKey A function that takes an index of an element in the * collection and returns zero if the value at this index is equal to the * search key, a negative number if the value precedes the search key in the * sorting order, or a positive number if the search key precedes the value. * @return A non-negative index of an element, if found. If not found, the * result is -(n+1) (or ~n, using bitwise notation), where n is the index * where the key should be inserted to maintain the sorting order. */ export function binarySearch2(length: number, compareToKey: (index: number) => number): number { let low = 0 let high = length - 1 while (low <= high) { const mid = ((low + high) / 2) | 0 const comp = compareToKey(mid) if (comp < 0) { low = mid + 1 } else if (comp > 0) { high = mid - 1 } else { return mid } } return -(low + 1) } export function intersect(array: T[], other: T[]): boolean { for (let item of other) { if (array.includes(item)) { return true } } return false } export function findIndex(array: ArrayLike, val: T, start = 0): number { let idx = -1 for (let i = start; i < array.length; i++) { if (array[i] === val) { idx = i break } } return idx } export function splitArray(array: T[], fn: (item: T) => boolean): [T[], T[]] { let res: [T[], T[]] = [[], []] for (let item of array) { if (fn(item)) { res[0].push(item) } else { res[1].push(item) } } return res } export function tail(array: T[], n = 0): T { return array[array.length - (1 + n)] } export function group(array: T[], size: number): T[][] { let len = array.length let res: T[][] = [] for (let i = 0; i < Math.ceil(len / size); i++) { res.push(array.slice(i * size, (i + 1) * size)) } return res } export function groupBy(array: T[], fn: (v: T) => boolean): [T[], T[]] { let res: [T[], T[]] = [[], []] array.forEach(v => { if (fn(v)) { res[0].push(v) } else { res[1].push(v) } }) return res } /** * Removes duplicates from the given array. The optional keyFn allows to specify * how elements are checked for equalness by returning a unique string for each. */ export function distinct(array: T[], keyFn?: (t: T) => string): T[] { if (!keyFn) { return array.filter((element, position) => array.indexOf(element) === position) } const seen: { [key: string]: boolean } = Object.create(null) return array.filter(elem => { const key = keyFn(elem) if (seen[key]) { return false } seen[key] = true return true }) } export function lastIndex(array: T[], fn: (t: T) => boolean): number { let i = array.length - 1 while (i >= 0) { if (fn(array[i])) { break } i-- } return i } export const flatMap = (xs: T[], f: (item: T) => U[]): U[] => xs.reduce((x: U[], y: T) => [...x, ...f(y)], []) /** * Add text to sorted array */ export function addSortedArray(text: string, arr: string[]): string[] { let idx: number for (let i = 0; i < arr.length; i++) { let s = arr[i] if (text === s) return arr if (s > text) { idx = i break } } if (idx === undefined) { arr.push(text) } else { arr.splice(idx, 0, text) } return arr } ================================================ FILE: src/util/async.ts ================================================ import { CancellationToken } from '../util/protocol' const defaultYieldTimeout = 15 function waitImmediate(): Promise { return new Promise(resolve => { setImmediate(() => { resolve(undefined) }) }) } class Timer { private readonly yieldAfter: number private startTime: number private counter: number private total: number private counterInterval: number constructor(yieldAfter: number = defaultYieldTimeout) { this.yieldAfter = Math.max(yieldAfter, defaultYieldTimeout) this.startTime = Date.now() this.counter = 0 this.total = 0 // start with a counter interval of 1. this.counterInterval = 1 } public start() { this.startTime = Date.now() } public shouldYield(): boolean { if (++this.counter >= this.counterInterval) { const timeTaken = Date.now() - this.startTime const timeLeft = Math.max(0, this.yieldAfter - timeTaken) this.total += this.counter this.counter = 0 if (timeTaken >= this.yieldAfter || timeLeft <= 1) { // Yield also if time left <= 1 since we compute the counter // for max < 2 ms. // Start with interval 1 again. We could do some calculation // with using 80% of the last counter however other things (GC) // affect the timing heavily since we have small timings (1 - 15ms). this.counterInterval = 1 this.total = 0 return true } else { // Only increase the counter until we have spent <= 2 ms. Increasing // the counter further is very fragile since timing is influenced // by other things and can increase the counter too much. This will result // that we yield in average after [14 - 16]ms. switch (timeTaken) { case 0: case 1: this.counterInterval = this.total * 2 break } } } return false } } export async function runSequence(funcs: (() => Promise)[], token: CancellationToken): Promise { for (const fn of funcs) { if (token.isCancellationRequested) { break } await fn() } } export interface YieldOptions { /** * The time in ms after which the function should yield. * The minimum yield time is 15ms */ yieldAfter?: number /* ms */ /** * An optional callback that is invoke when the code yields. */ yieldCallback?: () => void } export async function map(items: ReadonlyArray

, func: (item: P) => C, token?: CancellationToken, options?: YieldOptions): Promise { if (items.length === 0) { return [] } const result: C[] = new Array(items.length) const timer = new Timer(options?.yieldAfter) function convertBatch(start: number): number { timer.start() for (let i = start; i < items.length; i++) { result[i] = func(items[i]) if (timer.shouldYield()) { if (options?.yieldCallback) { options.yieldCallback() } return i + 1 } } return -1 } // Convert the first batch sync on the same frame. let index = convertBatch(0) while (index !== -1) { await waitImmediate() if (token !== undefined && token.isCancellationRequested) { return result } index = convertBatch(index) } return result } export async function forEach

(items: ReadonlyArray

, func: (item: P) => void, token?: CancellationToken, options?: YieldOptions): Promise { if (items.length === 0) { return } const timer = new Timer(options?.yieldAfter) function runBatch(start: number): number { timer.start() for (let i = start; i < items.length; i++) { func(items[i]) if (timer.shouldYield()) { if (options?.yieldCallback) { options.yieldCallback() } return i + 1 } } return -1 } // Convert the first batch sync on the same frame. let index = runBatch(0) while (index !== -1) { await waitImmediate() if (token !== undefined && token.isCancellationRequested) { break } index = runBatch(index) } } export async function filter

(items: ReadonlyArray

, isValid: (item: P) => boolean | { [key: string]: any }, onFilter: (items: (P & { [key: string]: any })[], done: boolean) => void, token?: CancellationToken): Promise { if (items.length === 0) return const timer = new Timer() const len = items.length function convertBatch(start: number): number { const result: P[] = [] timer.start() for (let i = start; i < len; i++) { let item = items[i] let res = isValid(item) if (res === true) { result.push(item) } else if (res) { result.push(Object.assign({}, item, res)) } if (timer.shouldYield()) { let done = i === len - 1 onFilter(result, done) return done ? -1 : i + 1 } } onFilter(result, true) return -1 } // Convert the first batch sync on the same frame. let index = convertBatch(0) while (index !== -1) { await waitImmediate() if (token != null && token.isCancellationRequested) { break } index = convertBatch(index) } } ================================================ FILE: src/util/charCode.ts ================================================ 'use strict' /* --------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ /** * An inlined enum containing useful character codes (to be used with String.charCodeAt). * Please leave the const keyword such that it gets inlined when compiled to JavaScript! */ export const enum CharCode { Null = 0, /** * The `\b` character. */ Backspace = 8, /** * The `\t` character. */ Tab = 9, /** * The `\n` character. */ LineFeed = 10, /** * The `\r` character. */ CarriageReturn = 13, Space = 32, /** * The `!` character. */ ExclamationMark = 33, /** * The `"` character. */ DoubleQuote = 34, /** * The `#` character. */ Hash = 35, /** * The `$` character. */ DollarSign = 36, /** * The `%` character. */ PercentSign = 37, /** * The `&` character. */ Ampersand = 38, /** * The `'` character. */ SingleQuote = 39, /** * The `(` character. */ OpenParen = 40, /** * The `)` character. */ CloseParen = 41, /** * The `*` character. */ Asterisk = 42, /** * The `+` character. */ Plus = 43, /** * The `,` character. */ Comma = 44, /** * The `-` character. */ Dash = 45, /** * The `.` character. */ Period = 46, /** * The `/` character. */ Slash = 47, Digit0 = 48, Digit1 = 49, Digit2 = 50, Digit3 = 51, Digit4 = 52, Digit5 = 53, Digit6 = 54, Digit7 = 55, Digit8 = 56, Digit9 = 57, /** * The `:` character. */ Colon = 58, /** * The `;` character. */ Semicolon = 59, /** * The `<` character. */ LessThan = 60, /** * The `=` character. */ Equals = 61, /** * The `>` character. */ GreaterThan = 62, /** * The `?` character. */ QuestionMark = 63, /** * The `@` character. */ AtSign = 64, A = 65, B = 66, C = 67, D = 68, E = 69, F = 70, G = 71, H = 72, I = 73, J = 74, K = 75, L = 76, M = 77, N = 78, O = 79, P = 80, Q = 81, R = 82, S = 83, T = 84, U = 85, V = 86, W = 87, X = 88, Y = 89, Z = 90, /** * The `[` character. */ OpenSquareBracket = 91, /** * The `\` character. */ Backslash = 92, /** * The `]` character. */ CloseSquareBracket = 93, /** * The `^` character. */ Caret = 94, /** * The `_` character. */ Underline = 95, /** * The ``(`)`` character. */ BackTick = 96, a = 97, b = 98, c = 99, d = 100, e = 101, f = 102, g = 103, h = 104, i = 105, j = 106, k = 107, l = 108, m = 109, n = 110, o = 111, p = 112, q = 113, r = 114, s = 115, t = 116, u = 117, v = 118, w = 119, x = 120, y = 121, z = 122, /** * The `{` character. */ OpenCurlyBrace = 123, /** * The `|` character. */ Pipe = 124, /** * The `}` character. */ CloseCurlyBrace = 125, /** * The `~` character. */ Tilde = 126, U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde U_Combining_Macron = 0x0304, // U+0304 Combining Macron U_Combining_Overline = 0x0305, // U+0305 Combining Overline U_Combining_Breve = 0x0306, // U+0306 Combining Breve U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent U_Combining_Caron = 0x030C, // U+030C Combining Caron U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above U_Combining_Horn = 0x031B, // U+031B Combining Horn U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below U_Combining_X_Above = 0x033D, // U+033D Combining X Above U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata U_Combining_X_Below = 0x0353, // U+0353 Combining X Below U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X /** * Unicode Character 'LINE SEPARATOR' (U+2028) * http://www.fileformat.info/info/unicode/char/2028/index.htm */ LINE_SEPARATOR_2028 = 8232, // http://www.fileformat.info/info/unicode/category/Sk/list.htm // U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX // U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS U_MACRON = 0x00AF, // U+00AF MACRON U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT U_CEDILLA = 0x00B8, // U+00B8 CEDILLA U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN U_BREVE = 0x02D8, // U+02D8 BREVE U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE U_OGONEK = 0x02DB, // U+02DB OGONEK U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE' /** * UTF-8 BOM * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) * http://www.fileformat.info/info/unicode/char/feff/index.htm */ UTF8_BOM = 65279 } ================================================ FILE: src/util/color.ts ================================================ 'use strict' import { Color } from 'vscode-languageserver-types' function pad(str: string): string { return str.length == 1 ? `0${str}` : str } export function toHexString(color: Color): string { let c = toHexColor(color) return `${pad(c.red.toString(16))}${pad(c.green.toString(16))}${pad(c.blue.toString(16))}` } function toHexColor(color: Color): { red: number; green: number; blue: number } { let { red, green, blue } = color return { red: Math.round(red * 255), green: Math.round(green * 255), blue: Math.round(blue * 255) } } export function isDark(color: Color): boolean { // http://www.w3.org/TR/WCAG20/#relativeluminancedef let rgb = [color.red, color.green, color.blue] let lum = [] for (let i = 0; i < rgb.length; i++) { let chan = rgb[i] lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4) } let luma = 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2] return luma <= 0.5 } ================================================ FILE: src/util/constants.ts ================================================ import { version } from '../../package.json' import { defaultValue, getConditionValue } from './index' import { os, path } from './node' export const ASCII_END = 128 export const VERSION = version export const isVim = process.env.VIM_NODE_RPC == '1' export const APIVERSION = 38 export const floatHighlightGroup = 'CocFloating' export const CONFIG_FILE_NAME = 'coc-settings.json' export const configHome = defaultValue(process.env.COC_VIMCONFIG, path.join(os.homedir(), '.vim')) export const dataHome = defaultValue(process.env.COC_DATA_HOME, path.join(os.homedir(), '.config/coc')) export const userConfigFile = path.join(path.normalize(configHome), CONFIG_FILE_NAME) export const pluginRoot = getConditionValue(path.dirname(__dirname), path.resolve(__dirname, '../..')) ================================================ FILE: src/util/convert.ts ================================================ 'use strict' import { FormattingOptions, SymbolKind } from 'vscode-languageserver-types' export interface VimFormatOption { tabsize: number expandtab: number insertFinalNewline: boolean trimTrailingWhitespace: boolean trimFinalNewlines: boolean } export function convertFormatOptions(opts: VimFormatOption): FormattingOptions { let obj: FormattingOptions = { tabSize: opts.tabsize, insertSpaces: opts.expandtab == 1 } if (opts.insertFinalNewline) obj.insertFinalNewline = true if (opts.trimTrailingWhitespace) obj.trimTrailingWhitespace = true if (opts.trimFinalNewlines) obj.trimFinalNewlines = true return obj } export function getSymbolKind(kind: SymbolKind): string { switch (kind) { case SymbolKind.File: return 'File' case SymbolKind.Module: return 'Module' case SymbolKind.Namespace: return 'Namespace' case SymbolKind.Package: return 'Package' case SymbolKind.Class: return 'Class' case SymbolKind.Method: return 'Method' case SymbolKind.Property: return 'Property' case SymbolKind.Field: return 'Field' case SymbolKind.Constructor: return 'Constructor' case SymbolKind.Enum: return 'Enum' case SymbolKind.Interface: return 'Interface' case SymbolKind.Function: return 'Function' case SymbolKind.Variable: return 'Variable' case SymbolKind.Constant: return 'Constant' case SymbolKind.String: return 'String' case SymbolKind.Number: return 'Number' case SymbolKind.Boolean: return 'Boolean' case SymbolKind.Array: return 'Array' case SymbolKind.Object: return 'Object' case SymbolKind.Key: return 'Key' case SymbolKind.Null: return 'Null' case SymbolKind.EnumMember: return 'EnumMember' case SymbolKind.Struct: return 'Struct' case SymbolKind.Event: return 'Event' case SymbolKind.Operator: return 'Operator' case SymbolKind.TypeParameter: return 'TypeParameter' default: return 'Unknown' } } ================================================ FILE: src/util/diff.ts ================================================ 'use strict' import { Position, Range, TextEdit } from 'vscode-languageserver-types' import { fastDiff } from './node' import { emptyRange, getEnd, positionInRange } from './position' import { byteLength } from './string' export interface ChangedLines { start: number end: number replacement: string[] } export function diffLines(oldLines: ReadonlyArray, newLines: ReadonlyArray, startLine: number): ChangedLines { let endOffset = 0 let startOffset = 0 let parts = oldLines.slice(startLine + 1) for (let i = 0; i < Math.min(parts.length, newLines.length); i++) { if (parts[parts.length - 1 - i] == newLines[newLines.length - 1 - i]) { endOffset = endOffset + 1 } else { break } } for (let i = 0; i <= Math.min(startLine, newLines.length - 1 - endOffset); i++) { if (oldLines[i] == newLines[i]) { startOffset = startOffset + 1 } else { break } } let replacement = newLines.slice(startOffset, newLines.length - endOffset) let end = oldLines.length - endOffset if (end > startOffset && replacement.length) { let offset = 0 for (let i = 0; i < Math.min(replacement.length, end - startOffset); i++) { if (replacement[i] == oldLines[startOffset + i]) { offset = offset + 1 } else { break } } if (offset) { return { start: startOffset + offset, end, replacement: replacement.slice(offset) } } } return { start: startOffset, end, replacement } } export function patchLine(from: string, to: string, fill = ' '): string { if (from == to) return to let idx = to.indexOf(from) if (idx !== -1) return fill.repeat(byteLength(to.substring(0, idx))) + from let result = fastDiff(from, to) let str = '' for (let item of result) { if (item[0] == fastDiff.DELETE) { // not allowed return to } else if (item[0] == fastDiff.INSERT) { str = str + fill.repeat(byteLength(item[1])) } else { str = str + item[1] } } return str } export function getTextEdit(oldLines: ReadonlyArray, newLines: ReadonlyArray, cursor?: Position, insertMode?: boolean): TextEdit | undefined { let ol = oldLines.length let nl = newLines.length let n: number if (cursor) { // consider new line insert n = nl > ol && insertMode && cursor.line > 0 ? cursor.line - 1 : cursor.line } else { n = Math.min(ol, nl) } let used = 0 for (let i = 0; i < n; i++) { if (newLines[i] === oldLines[i]) { used += 1 } else { break } } if (ol == nl && used == ol) return undefined let delta = nl - ol let r = Math.min(ol - used, nl - used) let e = 0 for (let i = 0; i < r; i++) { if (newLines[nl - i - 1] === oldLines[ol - i - 1]) { e += 1 } else { break } } let inserted = e == 0 ? newLines.slice(used) : newLines.slice(used, -e) if (delta == 0 && cursor && inserted.length == 1) { let newLine = newLines[used] let oldLine = oldLines[used] let nl = newLine.length let ol = oldLine.length if (nl === 0) return TextEdit.del(Range.create(used, 0, used, ol)) if (ol === 0) return TextEdit.insert(Position.create(used, 0), newLine) let character = Math.min(cursor.character, nl) if (!insertMode && nl >= ol && character !== nl) { // insert text character += 1 } let r = 0 for (let i = 0; i < nl - character; i++) { let idx = ol - 1 - i if (idx === -1) break if (newLine[nl - 1 - i] === oldLine[idx]) { r += 1 } else { break } } let l = 0 for (let i = 0; i < Math.min(ol - r, nl - r); i++) { if (newLine[i] === oldLine[i]) { l += 1 } else { break } } let newText = r === 0 ? newLine.slice(l) : newLine.slice(l, -r) return TextEdit.replace(Range.create(used, l, used, ol - r), newText) } let text = inserted.length > 0 ? inserted.join('\n') + '\n' : '' if (text.length === 0 && used === ol - e) return undefined let original = oldLines.slice(used, ol - e).join('\n') + '\n' let edit = TextEdit.replace(Range.create(used, 0, ol - e, 0), text) return reduceReplaceEdit(edit, original, cursor) } export function getCommonSuffixLen(a: string, b: string, max: number): number { if (max === 0) return 0 let al = a.length let bl = b.length let n = 0 for (let i = 0; i < max; i++) { if (a[al - 1 - i] === b[bl - 1 - i]) { n++ } else { break } } return n } export function getCommonPrefixLen(a: string, b: string, max: number): number { if (max === 0) return 0 let n = 0 for (let i = 0; i < max; i++) { if (a[i] === b[i]) { n++ } else { break } } return n } export function reduceReplaceEdit(edit: TextEdit, original: string, cursor?: Position): TextEdit { let { newText, range } = edit if (emptyRange(range) || newText === '') return edit // let isAdd = newText.length > original.length let endOffset: number | undefined if (cursor) { let newEnd = getEnd(range.start, newText) if (positionInRange(cursor, Range.create(range.start, newEnd)) === 0) { endOffset = 0 let lc = newEnd.line - cursor.line + 1 let lines = newText.split('\n') let len = lines.length for (let i = 0; i < lc; i++) { let idx = len - i - 1 if (i == lc - 1) { let s = idx === 0 ? range.start.character : 0 endOffset += lines[idx].slice(cursor.character - s).length } else { endOffset += lines[idx].length + 1 } } } } let sl: number let pl: number let min = Math.min(original.length, newText.length) if (endOffset) { sl = getCommonSuffixLen(original, newText, endOffset) pl = getCommonPrefixLen(original, newText, min - sl) } else { pl = getCommonPrefixLen(original, newText, min) sl = getCommonSuffixLen(original, newText, min - pl) } let s = pl === 0 ? range.start : getEnd(range.start, original.slice(0, pl)) let e = sl === 0 ? range.end : getEnd(range.start, original.slice(0, -sl)) let text = newText.slice(pl, sl === 0 ? undefined : -sl) return TextEdit.replace(Range.create(s, e), text) } ================================================ FILE: src/util/errors.ts ================================================ 'use strict' const canceledName = 'Canceled' // !!!IMPORTANT!!! // Do NOT change this class because it is also used as an API-type. export class CancellationError extends Error { constructor() { super(canceledName) this.name = this.message } } export function assert(condition: boolean): void { if (!condition) { throw new BugIndicatingError('Assertion Failed') } } /** * This error indicates a bug. * Do not throw this for invalid user input. * Only catch this error to recover gracefully from bugs. */ class BugIndicatingError extends Error { constructor(message: string) { super(message) Object.setPrototypeOf(this, BugIndicatingError.prototype) // Because we know for sure only buggy code throws this, // we definitely want to break here and fix the bug. // eslint-disable-next-line no-debugger debugger } } /** * Checks if the given error is a promise in canceled state */ export function isCancellationError(error: any): boolean { if (error instanceof CancellationError) { return true } return error instanceof Error && error.name === canceledName && error.message === canceledName } export function shouldIgnore(err: any) { if (isCancellationError(err)) return true if (err instanceof Error && err.message.includes('transport disconnected')) return true return false } export function onUnexpectedError(e: any): void { // ignore errors from cancelled promises if (shouldIgnore(e)) return if (e.stack) { throw new Error(e.message + '\n\n' + e.stack) } else { throw e } } export function notLoaded(uri: string): Error { return new Error(`File ${uri} not loaded`) } export function illegalArgument(name?: string): Error { if (name) { return new Error(`Illegal argument: ${name}`) } else { return new Error('Illegal argument') } } export function directoryNotExists(dir: string): Error { return new Error(`Directory ${dir} not exists`) } export function fileExists(filepath: string) { return new Error(`File ${filepath} already exists`) } export function fileNotExists(filepath: string) { return new Error(`File ${filepath} not exists`) } export function shouldNotAsync(method: string) { return new Error(`${method} should not be called in an asynchronize manner`) } export function badScheme(uri: string) { return new Error(`Change of ${uri} not supported`) } ================================================ FILE: src/util/extensionRegistry.ts ================================================ 'use strict' import { isFalsyOrEmpty, toArray } from './array' import { pluginRoot } from './constants' import { isParentFolder, sameFile } from './fs' import * as Is from './is' import type { IJSONSchema } from './jsonSchema' import { fs } from './node' import { toObject } from './object' import { Registry } from './registry' import { toText } from './string' export type IStringDictionary = Record /** * Contains static extension infos */ export const Extensions = { ExtensionContribution: 'base.contributions.extensions' } export interface CommandContribution { readonly title: string readonly command: string } export interface RootPatternContrib { readonly filetype: string readonly patterns?: string[] } export interface IExtensionInfo { readonly name: string, readonly directory: string, readonly filepath?: string readonly onCommands?: string[] readonly commands?: CommandContribution[] readonly rootPatterns?: RootPatternContrib[] readonly definitions?: IStringDictionary } export interface IExtensionContributions { extensions: Iterable } export interface IExtensionRegistry { /** * Commands for activate extensions. */ readonly onCommands: ({ id: string, title: string })[] /** * Commands contributed from extensions. */ readonly commands: CommandContribution[] getCommandTitle(id: string): string | undefined /** * Root patterns by filetype. */ getRootPatternsByFiletype(filetype: string): string[] /** * Register a extension to the registry. */ registerExtension(id: string, info: IExtensionInfo): void /** * Remove a extension from registry */ unregistExtension(id: string): void /** * Get extension info. */ getExtension(id: string): IExtensionInfo /** * Get all extensions */ getExtensions(): IExtensionContributions resolveExtension(filepath: string): IExtensionInfo | undefined } /** * Registry for loaded extensions. */ class ExtensionRegistry implements IExtensionRegistry { private extensionsById: Map constructor() { this.extensionsById = new Map() } public resolveExtension(filepath: string): IExtensionInfo { for (let item of this.extensionsById.values()) { if (item.filepath && sameFile(item.filepath, filepath)) { return item } if (!item.name.startsWith('single-') && fs.existsSync(item.directory) && isParentFolder(fs.realpathSync(item.directory), filepath, false)) { return item } } return undefined } public get onCommands(): ({ id: string, title: string })[] { let res: ({ id: string, title: string })[] = [] for (let item of this.extensionsById.values()) { let { commands, onCommands } = item for (let cmd of onCommands) { if (typeof cmd === 'string') { let find = commands.find(o => o.command === cmd) let title = find == null ? '' : find.title res.push({ id: cmd, title }) } } } return res } public getCommandTitle(id: string): string | undefined { for (let item of this.extensionsById.values()) { for (let cmd of toArray(item.commands)) { if (cmd.command === id) return cmd.title } } return undefined } public get commands(): CommandContribution[] { let res: CommandContribution[] = [] for (let item of this.extensionsById.values()) { res.push(...(toArray(item.commands).filter(validCommandContribution))) } return res } public getRootPatternsByFiletype(filetype: string): string[] { let res: string[] = [] for (let item of this.extensionsById.values()) { for (let p of toArray(item.rootPatterns).filter(validRootPattern)) { if (p.filetype === filetype) res.push(...p.patterns.filter(s => typeof s === 'string')) } } return res } public unregistExtension(id: string): void { this.extensionsById.delete(id) } public registerExtension(id: string, info: IExtensionInfo): void { this.extensionsById.set(id, info) } public getExtension(id: string): IExtensionInfo { return this.extensionsById.get(id) } public getExtensions(): IExtensionContributions { return { extensions: this.extensionsById.values() } } } let extensionRegistry = new ExtensionRegistry() Registry.add(Extensions.ExtensionContribution, extensionRegistry) export function getExtensionDefinitions(): IStringDictionary { let obj = {} for (let extensionInfo of extensionRegistry.getExtensions().extensions) { let definitions = extensionInfo.definitions Object.entries(toObject(definitions)).forEach(([key, val]) => { obj[key] = val }) } return obj } export function validRootPattern(rootPattern: RootPatternContrib | undefined): boolean { return rootPattern && typeof rootPattern.filetype === 'string' && !isFalsyOrEmpty(rootPattern.patterns) } export function validCommandContribution(cmd: CommandContribution | undefined): boolean { return cmd && typeof cmd.command === 'string' && typeof cmd.title === 'string' } export function getProperties(configuration: object): IStringDictionary { let obj = {} if (Array.isArray(configuration)) { for (let item of configuration) { Object.assign(obj, toObject(item['properties'])) } } else if (Is.objectLiteral(configuration['properties'])) { obj = configuration['properties'] } return obj } /** * Get extension name from error stack */ export function parseExtensionName(stack: string, level = 2): string | undefined { let lines = toText(stack).split(/\r?\n/).slice(level) if (lines.length === 0) return undefined for (let line of lines) { let filepath: string | undefined line = line.replace(/^\s*at\s*/, '') if (line.endsWith(')')) { let ms = line.match(/(\((.*?):\d+:\d+\))$/) if (ms) filepath = ms[2] } else { let ms = line.match(/(.*?):\d+:\d+$/) if (ms) filepath = ms[1] } if (!filepath || isParentFolder(pluginRoot, filepath)) continue let find = extensionRegistry.resolveExtension(filepath) if (find) return find.name } return 'coc.nvim' } ================================================ FILE: src/util/factory.ts ================================================ 'use strict' import { createLogger } from '../logger' import { fs, path, vm } from '../util/node' import { hasOwnProperty, toObject } from './object' export interface ExtensionExport { activate: (context: unknown) => any deactivate?: () => any [key: string]: any } export interface ILogger { category?: string log(...args: any[]): void trace(...args: any[]): void debug(...args: any[]): void info(...args: any[]): void warn(...args: any[]): void error(...args: any[]): void fatal(...args: any[]): void mark(...args: any[]): void } export interface IModule { new(name: string, parent?: boolean): any _resolveFilename: (file: string, context: any, isMain: boolean, options: any) => string _extensions: object _cache: { [file: string]: any } _compile: (content: string, filename: string) => any wrap: (content: string) => string require: (file: string) => NodeModule _nodeModulePaths: (filename: string) => string[] createRequire: (filename: string) => (file: string) => any } export const consoleLogger: ILogger = { category: '', log: console.log.bind(console), debug: console.debug.bind(console), error: console.error.bind(console), warn: console.warn.bind(console), info: console.info.bind(console), trace: console.log.bind(console), fatal: console.error.bind(console), mark: console.log.bind(console), } const Module: IModule = require('module') const mainModule = require.main const REMOVED_GLOBALS = [ 'reallyExit', 'abort', 'umask', 'setuid', 'setgid', 'setgroups', '_fatalException', 'exit', 'kill', ] function removedGlobalStub(name: string) { return () => { throw new Error(`process.${name}() is not allowed in extension sandbox`) } } // @see node/lib/internal/module.js function makeRequireFunction(this: any, cocExports: any): any { const req: any = (p: string) => { if (p === 'coc.nvim') { return toObject(cocExports) } return this.require(p) } req.resolve = (request, options) => Module._resolveFilename(request, this, false, options) // request => Module._resolveFilename(request, this) req.main = mainModule // Enable support to add extra extension types req.extensions = Module._extensions req.cache = Module._cache return req } // @see node/lib/module.js export function compileInSandbox(sandbox: ISandbox, cocExports?: any): (content: string, filename: string) => any { return function(this: any, content: string, filename: string): any { const require = makeRequireFunction.call(this, cocExports) const dirname = path.dirname(filename) const newContent = content.startsWith('#!') ? content.replace(/^#!.*/, '') : content const wrapper = Module.wrap(newContent) const compiledWrapper = vm.runInContext(wrapper, sandbox, { filename }) const args = [this.exports, require, this, filename, dirname] return compiledWrapper.apply(this.exports, args) } } export interface ISandbox { process: NodeJS.Process module: NodeModule require: (p: string) => any // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type console: { [key in keyof Console]?: Function } Buffer: any Reflect: any // eslint-disable-next-line id-blacklist String: any Promise: any } // find correct Module since jest use a fake Module object that extends Module // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function getProtoWithCompile(mod: Function): IModule { if (hasOwnProperty(mod.prototype, '_compile')) return mod.prototype if (hasOwnProperty(mod.prototype.__proto__, '_compile')) return mod.prototype.__proto__ throw new Error('_compile not found') } const ModuleProto = getProtoWithCompile(Module) export function copyGlobalProperties(sandbox: ISandbox, globalObj: any): ISandbox { // Use Reflect.ownKeys affect instanceof of extensions, instanceof Error and instanceof TypeError won't work for (const key of Object.keys(globalObj)) { const value = sandbox[key] if (value === undefined) { sandbox[key] = globalObj[key] } } return sandbox } export function createConsole(con: object, logger: ILogger): object { let result: any = {} let methods = ['debug', 'log', 'info', 'error', 'warn'] for (let key of Object.keys(con)) { if (methods.includes(key)) { result[key] = (...args: any[]) => { logger[key].apply(logger, args) } } else { let fn = con[key] if (key !== 'Console' && typeof fn === 'function') { result[key] = () => { logger.warn(`function console.${key} not supported`) } } else { result[key] = fn } } } return result } export function createSandbox(filename: string, logger: ILogger, name?: string, noExport = global.__TEST__): ISandbox { const module = new Module(filename) module.paths = Module._nodeModulePaths(filename) const sandbox = vm.createContext({ module, Buffer, URL: globalThis.URL, WebAssembly: globalThis.WebAssembly, console: createConsole(console, logger) }, { name }) as ISandbox copyGlobalProperties(sandbox, global) // sandbox.Reflect = Reflect let cocExports = noExport ? undefined : require('../index') sandbox.require = function sandboxRequire(p): any { const oldCompile = ModuleProto._compile ModuleProto._compile = compileInSandbox(sandbox, cocExports) const moduleExports = sandbox.module.require(p) ModuleProto._compile = oldCompile return moduleExports } // patch `require` in sandbox to run loaded module in sandbox context // if you need any of these, it might be worth discussing spawning separate processes sandbox.process = new (process as any).constructor() for (let key of Reflect.ownKeys(process)) { if (typeof key === 'string' && key.startsWith('_')) { continue } sandbox.process[key] = process[key] } REMOVED_GLOBALS.forEach(name => { sandbox.process[name] = removedGlobalStub(name) }) sandbox.process['chdir'] = () => {} // read-only umask sandbox.process.umask = (mask?: number) => { if (typeof mask !== 'undefined') { throw new Error('Cannot use process.umask() to change mask (read-only)') } return process.umask() } return sandbox } function getLogger(useConsole: boolean, id: string): ILogger { return useConsole ? consoleLogger : createLogger(`extension:${id}`) } // inspiration drawn from Module export function createExtension(id: string, filename: string, isEmpty: boolean): ExtensionExport { if (isEmpty || !fs.existsSync(filename)) return { activate: () => {}, deactivate: null } const logger = getLogger(!global.__isMain && !global.__TEST__, id) const sandbox = createSandbox(filename, logger, id) delete Module._cache[require.resolve(filename)] // attempt to import plugin // Require plugin to export activate & deactivate const defaultImport = sandbox.require(filename) const activate = (defaultImport && defaultImport.activate) || defaultImport if (typeof activate !== 'function') return { activate: () => {} } return typeof defaultImport === 'function' ? { activate } : Object.assign({}, defaultImport) } ================================================ FILE: src/util/filter.ts ================================================ import { CharCode } from './charCode' import { isEmojiImprecise } from './string' /** * An array representing a fuzzy match. * * 0. the score * 1. the offset at which matching started * 2. `` * 3. `` * 4. `` etc */ export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]] const _maxLen = 128 const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 } export interface FuzzyScoreOptions { readonly boostFullMatch: boolean readonly firstMatchCanBeWeak: boolean } export interface IMatch { start: number end: number } export interface FuzzyScorer { (pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined } function initTable() { const table: number[][] = [] const row: number[] = [] for (let i = 0; i <= _maxLen; i++) { row[i] = 0 } for (let i = 0; i <= _maxLen; i++) { table.push(row.slice(0)) } return table } function initArr(maxLen: number) { const row: number[] = [] for (let i = 0; i <= maxLen; i++) { row[i] = 0 } return row } function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean { return word[pos] !== wordLow[pos] } const _minWordMatchPos = initArr(2 * _maxLen) // min word position for a certain pattern position const _maxWordMatchPos = initArr(2 * _maxLen) // max word position for a certain pattern position const _diag = initTable() // the length of a contiguous diagonal match const _table = initTable() const _arrows = initTable() as Arrow[][] export function fuzzyScoreGracefulAggressive(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined { return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, true, options) } export function fuzzyScoreGraceful(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined { return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, false, options) } function fuzzyScoreWithPermutations(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, aggressive: boolean, options?: FuzzyScoreOptions): FuzzyScore | undefined { let top = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, options) if (top && !aggressive) { // when using the original pattern yield a result we` // return it unless we are aggressive and try to find // a better alignment, e.g. `cno` -> `^co^ns^ole` or `^c^o^nsole`. return top } if (pattern.length >= 3) { // When the pattern is long enough then try a few (max 7) // permutations of the pattern to find a better match. The // permutations only swap neighbouring characters, e.g // `cnoso` becomes `conso`, `cnsoo`, `cnoos`. const tries = Math.min(7, pattern.length - 1) for (let movingPatternPos = patternPos + 1; movingPatternPos < tries; movingPatternPos++) { const newPattern = nextTypoPermutation(pattern, movingPatternPos) if (newPattern) { const candidate = fuzzyScore(newPattern, newPattern.toLowerCase(), patternPos, word, lowWord, wordPos, options) if (candidate) { candidate[0] -= 3 // permutation penalty if (!top || candidate[0] > top[0]) { top = candidate } } } } } return top } export function fuzzyScore(pattern: string, patternLow: string, patternStart: number, word: string, wordLow: string, wordStart: number, options: FuzzyScoreOptions = { boostFullMatch: true, firstMatchCanBeWeak: false }): FuzzyScore | undefined { const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length const wordLen = word.length > _maxLen ? _maxLen : word.length if (patternStart >= patternLen || wordStart >= wordLen || (patternLen - patternStart) > (wordLen - wordStart)) { return undefined } // Run a simple check if the characters of pattern occur // (in order) at all in word. If that isn't the case we // stop because no match will be possible if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen, true)) { return undefined } // Find the max matching word position for each pattern position // NOTE: the min matching word position was filled in above, in the `isPatternInWord` call _fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow) let row = 1 let column = 1 let patternPos = patternStart let wordPos = wordStart const hasStrongFirstMatch = [false] // There will be a match, fill in tables for (row = 1, patternPos = patternStart; patternPos < patternLen; row++, patternPos++) { // Reduce search space to possible matching word positions and to possible access from next row const minWordMatchPos = _minWordMatchPos[patternPos] const maxWordMatchPos = _maxWordMatchPos[patternPos] const nextMaxWordMatchPos = (patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen) for (column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos; wordPos < nextMaxWordMatchPos; column++, wordPos++) { let score = Number.MIN_SAFE_INTEGER let canComeDiag = false if (wordPos <= maxWordMatchPos) { score = _doScore( pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos, wordLen, wordStart, _diag[row - 1][column - 1] === 0, hasStrongFirstMatch ) } let diagScore = 0 canComeDiag = true diagScore = score + _table[row - 1][column - 1] const canComeLeft = wordPos > minWordMatchPos const leftScore = canComeLeft ? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0) : 0 // penalty for a gap start const canComeLeftLeft = wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0 const leftLeftScore = canComeLeftLeft ? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0) : 0 // penalty for a gap start if (canComeLeftLeft && (!canComeLeft || leftLeftScore >= leftScore) && (!canComeDiag || leftLeftScore >= diagScore)) { // always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word _table[row][column] = leftLeftScore _arrows[row][column] = Arrow.LeftLeft _diag[row][column] = 0 } else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) { // always prefer choosing left since that means a match is earlier in the word _table[row][column] = leftScore _arrows[row][column] = Arrow.Left _diag[row][column] = 0 } else { _table[row][column] = diagScore _arrows[row][column] = Arrow.Diag _diag[row][column] = _diag[row - 1][column - 1] + 1 } } } // if (_debug) { // printTables(pattern, patternStart, word, wordStart) // } if (!hasStrongFirstMatch[0] && !options.firstMatchCanBeWeak) { return undefined } row-- column-- const result: FuzzyScore = [_table[row][column], wordStart] let backwardsDiagLength = 0 let maxMatchColumn = 0 while (row >= 1) { // Find the column where we go diagonally up let diagColumn = column do { const arrow = _arrows[row][diagColumn] if (arrow === Arrow.LeftLeft) { diagColumn = diagColumn - 2 } else if (arrow === Arrow.Left) { diagColumn = diagColumn - 1 } else { // found the diagonal break } } while (diagColumn >= 1) // Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match if ( backwardsDiagLength > 1 // only if we would have a contiguous match of 3 characters && patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] // only if we can do a contiguous match diagonally && !isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) // only if the forwards chose diagonal is not an uppercase && backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match ) { diagColumn = column } if (diagColumn === column) { // this is a contiguous match backwardsDiagLength++ } else { backwardsDiagLength = 1 } if (!maxMatchColumn) { // remember the last matched column maxMatchColumn = diagColumn } row-- column = diagColumn - 1 result.push(column) } if (wordLen === patternLen && options.boostFullMatch) { // the word matches the pattern with all characters! // giving the score a total match boost (to come up ahead other words) result[0] += 2 } // Add 1 penalty for each skipped character in the word const skippedCharsCount = maxMatchColumn - patternLen result[0] -= skippedCharsCount return result } export function anyScore(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore { const max = Math.min(13, pattern.length) for (; patternPos < max; patternPos++) { const result = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, options) if (result) { return result } } return [0, wordPos] } export function createMatches(score: undefined | FuzzyScore): IMatch[] { if (typeof score === 'undefined') { return [] } const res: IMatch[] = [] const wordPos = score[1] for (let i = score.length - 1; i > 1; i--) { const pos = score[i] + wordPos const last = res[res.length - 1] if (last && last.end === pos) { last.end = pos + 1 } else { res.push({ start: pos, end: pos + 1 }) } } return res } function _doScore( pattern: string, patternLow: string, patternPos: number, patternStart: number, word: string, wordLow: string, wordPos: number, wordLen: number, wordStart: number, newMatchStart: boolean, outFirstMatchStrong: boolean[], ): number { if (patternLow[patternPos] !== wordLow[wordPos]) { return Number.MIN_SAFE_INTEGER } let score = 1 let isGapLocation = false if (wordPos === (patternPos - patternStart)) { // common prefix: `foobar <-> foobaz` // ^^^^^ score = pattern[patternPos] === word[wordPos] ? 7 : 5 } else if (isUpperCaseAtPos(wordPos, word, wordLow) && (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))) { // hitting upper-case: `foo <-> forOthers` // ^^ ^ score = pattern[patternPos] === word[wordPos] ? 7 : 5 isGapLocation = true } else if (isSeparatorAtPos(wordLow, wordPos) && (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))) { // hitting a separator: `. <-> foo.bar` // ^ score = 5 } else if (isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1)) { // post separator: `foo <-> bar_foo` // ^^^ score = 5 isGapLocation = true } if (score > 1 && patternPos === patternStart) { outFirstMatchStrong[0] = true } if (!isGapLocation) { isGapLocation = isUpperCaseAtPos(wordPos, word, wordLow) || isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1) } // if (patternPos === patternStart) { // first character in pattern if (wordPos > wordStart) { // the first pattern character would match a word character that is not at the word start // so introduce a penalty to account for the gap preceding this match score -= isGapLocation ? 3 : 5 } } else { if (newMatchStart) { // this would be the beginning of a new match (i.e. there would be a gap before this location) score += isGapLocation ? 2 : 0 } else { // this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a preferred gap location score += isGapLocation ? 0 : 1 } } if (wordPos + 1 === wordLen) { // we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word // so pretend there is a gap after the last character in the word to normalize things score -= isGapLocation ? 3 : 5 } return score } export function isSeparatorAtPos(value: string, index: number): boolean { const code = value.codePointAt(index) switch (code) { case CharCode.Underline: case CharCode.Dash: case CharCode.Period: case CharCode.Space: case CharCode.Slash: case CharCode.Backslash: case CharCode.SingleQuote: case CharCode.DoubleQuote: case CharCode.Colon: case CharCode.DollarSign: case CharCode.LessThan: case CharCode.GreaterThan: case CharCode.OpenParen: case CharCode.CloseParen: case CharCode.OpenSquareBracket: case CharCode.CloseSquareBracket: case CharCode.OpenCurlyBrace: case CharCode.CloseCurlyBrace: return true case undefined: return false default: if (isEmojiImprecise(code)) { return true } return false } } export function isWhitespaceAtPos(value: string, index: number): boolean { if (index < 0 || index >= value.length) { return false } const code = value.charCodeAt(index) switch (code) { case CharCode.Space: case CharCode.Tab: return true default: return false } } export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number, fillMinWordPosArr = false): boolean { while (patternPos < patternLen && wordPos < wordLen) { if (patternLow[patternPos] === wordLow[wordPos]) { if (fillMinWordPosArr) { // Remember the min word position for each pattern position _minWordMatchPos[patternPos] = wordPos } patternPos += 1 } wordPos += 1 } return patternPos === patternLen // pattern must be exhausted } function _fillInMaxWordMatchPos(patternLen: number, wordLen: number, patternStart: number, wordStart: number, patternLow: string, wordLow: string) { let patternPos = patternLen - 1 let wordPos = wordLen - 1 while (patternPos >= patternStart && wordPos >= wordStart) { if (patternLow[patternPos] === wordLow[wordPos]) { _maxWordMatchPos[patternPos] = wordPos patternPos-- } wordPos-- } } export function nextTypoPermutation(pattern: string, patternPos: number): string | undefined { if (patternPos + 1 >= pattern.length) { return undefined } const swap1 = pattern[patternPos] const swap2 = pattern[patternPos + 1] if (swap1 === swap2) { return undefined } return pattern.slice(0, patternPos) + swap2 + swap1 + pattern.slice(patternPos + 2) } ================================================ FILE: src/util/fs.ts ================================================ 'use strict' import type { Stats } from 'fs' import { parse, ParseError } from 'jsonc-parser' import { Location, Position, Range } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import { createLogger } from '../logger' import { fs, path, promisify } from '../util/node' import { CancellationToken, Disposable } from '../util/protocol' import { isFalsyOrEmpty, toArray } from './array' import { CancellationError } from './errors' import { child_process, debounce, glob, minimatch, readline } from './node' import { toObject } from './object' import * as platform from './platform' import { smartcaseIndex } from './string' const logger = createLogger('util-fs') const exec = child_process.exec export enum FileType { /** * The file type is unknown. */ Unknown = 0, /** * A regular file. */ File = 1, /** * A directory. */ Directory = 2, /** * A symbolic link to a file. */ SymbolicLink = 64 } export type OnReadLine = (line: string) => void export function watchFile(filepath: string, onChange: () => void, immediate = false): Disposable { let callback = debounce(onChange, 100) try { let watcher = fs.watch(filepath, { persistent: true, recursive: false, encoding: 'utf8' }, () => { callback() }) if (immediate) { setTimeout(onChange, 10) } return Disposable.create(() => { callback.clear() watcher.close() }) } catch (e) { return Disposable.create(() => { callback.clear() }) } } export function loadJson(filepath: string): object { try { let errors: ParseError[] = [] let text = fs.readFileSync(filepath, 'utf8') let data = parse(text, errors, { allowTrailingComma: true }) if (errors.length > 0) { logger.error(`Error on parse json file ${filepath}`, errors) } return data ?? {} } catch (e) { return {} } } export function writeJson(filepath: string, obj: any): void { let dir = path.dirname(filepath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) logger.info(`Creating directory ${dir}`) } fs.writeFileSync(filepath, JSON.stringify(toObject(obj), null, 2), 'utf8') } export async function statAsync(filepath: string): Promise { let stat = null try { stat = await promisify(fs.stat)(filepath) } catch (e) {} return stat } export function isDirectory(filepath: string | undefined): boolean { if (!filepath || !path.isAbsolute(filepath) || !fs.existsSync(filepath)) return false let stat = fs.statSync(filepath) return stat.isDirectory() } export function renameAsync(oldPath: string, newPath: string): Promise { return new Promise((resolve, reject) => { fs.rename(oldPath, newPath, err => { if (err) return reject(err) resolve() }) }) } export async function remove(filepath: string | undefined): Promise { if (!filepath) return try { await promisify(fs.rm)(filepath, { force: true, recursive: true }) } catch (e) { return } } export async function getFileType(filepath: string): Promise { try { const stat = await promisify(fs.lstat)(filepath) if (stat.isFile()) { return FileType.File } if (stat.isDirectory()) { return FileType.Directory } if (stat.isSymbolicLink()) { return FileType.SymbolicLink } return FileType.Unknown } catch (e) { return undefined } } export async function isGitIgnored(fullpath: string | undefined): Promise { if (!fullpath) return false let stat = await statAsync(fullpath) if (!stat || !stat.isFile()) return false let root = null try { let { stdout } = await promisify(exec)('git rev-parse --show-toplevel', { cwd: path.dirname(fullpath) }) root = stdout.trim() } catch (e) {} if (!root) return false let file = path.relative(root, fullpath) try { let { stdout } = await promisify(exec)(`git check-ignore ${file}`, { cwd: root }) return stdout.trim() == file } catch (e) {} return false } export function isFolderIgnored(folder: string, ignored: string[] | undefined): boolean { if (isFalsyOrEmpty(ignored)) return false return ignored.some(p => sameFile(p, folder) || minimatch(folder, p, { dot: true })) } export function resolveRoot(folder: string, subs: ReadonlyArray, cwd?: string, bottomup = false, checkCwd = true, ignored: string[] = []): string | null { let dir = normalizeFilePath(folder) if (checkCwd && cwd && isParentFolder(cwd, dir, true) && !isFolderIgnored(cwd, ignored) && inDirectory(cwd, subs)) return cwd let parts = dir.split(path.sep) if (bottomup) { while (parts.length > 0) { let dir = parts.join(path.sep) if (!isFolderIgnored(dir, ignored) && inDirectory(dir, subs)) { return dir } parts.pop() } return null } else { let curr: string[] = [parts.shift()] for (let part of parts) { curr.push(part) let dir = curr.join(path.sep) if (!isFolderIgnored(dir, ignored) && inDirectory(dir, subs)) { return dir } } return null } } export function checkFolder(dir: string, patterns: string[], token?: CancellationToken): Promise { return new Promise(async (resolve, reject) => { if (isFalsyOrEmpty(patterns)) return resolve(false) let disposable: Disposable | undefined const ac = new AbortController() if (token) { disposable = token.onCancellationRequested(() => { ac.abort() reject(new CancellationError()) }) } let find = false let pattern = patterns.length == 1 ? patterns[0] : `{${patterns.join(',')}}` let gl = new glob.Glob(pattern, { nosort: true, signal: ac.signal, ignore: ['node_modules/**', '.git/**'], dot: true, cwd: dir, nodir: true, absolute: false }) try { for await (const _file of gl) { find = true break } } catch (e) { logger.error(`Error on glob "${pattern}"`, dir, e) } resolve(find) }) } export function inDirectory(dir: string, subs: ReadonlyArray): boolean { try { let files = fs.readdirSync(dir) for (let pattern of subs) { // note, only '*' expanded let is_wildcard = (pattern.includes('*')) let res = is_wildcard ? (minimatch.match(files, pattern, { nobrace: true, noext: true, nocomment: true, nonegate: true, dot: true }).length !== 0) : (files.includes(pattern)) if (res) return true } } catch (e) { // could be failed without permission } return false } /** * Find a matched file inside directory. */ export function findMatch(dir: string, subs: string[]): string | undefined { try { let files = fs.readdirSync(dir) for (let pattern of subs) { // note, only '*' expanded let isWildcard = (pattern.includes('*')) if (isWildcard) { let filtered = files.filter(minimatch.filter(pattern, { nobrace: true, noext: true, nocomment: true, nonegate: true, dot: true })) if (filtered.length > 0) return filtered[0] } else { let file = files.find(s => s === pattern) if (file) return file } } } catch (e) { // could be failed without permission } return undefined } export function findUp(name: string | string[], cwd: string): string { let root = path.parse(cwd).root let subs = toArray(name) while (cwd && cwd !== root) { let find = findMatch(cwd, subs) if (find) return path.join(cwd, find) cwd = path.dirname(cwd) } return null } export function readFile(fullpath: string, encoding: BufferEncoding): Promise { return new Promise((resolve, reject) => { fs.readFile(fullpath, encoding, (err, content) => { if (err) reject(err) resolve(content) }) }) } export function getFileLineCount(filepath: string): Promise { let i let count = 0 return new Promise((resolve, reject) => { fs.createReadStream(filepath) .on('error', e => reject(e)) .on('data', chunk => { for (i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++ }) .on('end', () => resolve(count)) }) } export function readFileLines(fullpath: string, start: number, end: number): Promise { if (!fs.existsSync(fullpath)) { return Promise.reject(new Error(`file does not exist: ${fullpath}`)) } let res: string[] = [] const input = fs.createReadStream(fullpath, { encoding: 'utf8' }) const rl = readline.createInterface({ input, crlfDelay: Infinity, terminal: false } as any) let n = 0 return new Promise((resolve, reject) => { rl.on('line', line => { if (n >= start && n <= end) { res.push(line) } if (n == end) { rl.close() } n = n + 1 }) rl.on('close', () => { resolve(res) input.close() }) rl.on('error', reject) }) } export function readFileLine(fullpath: string, count: number): Promise { if (!fs.existsSync(fullpath)) return Promise.reject(new Error(`file does not exist: ${fullpath}`)) const input = fs.createReadStream(fullpath, { encoding: 'utf8' }) const rl = readline.createInterface({ input, crlfDelay: Infinity, terminal: false } as any) let n = 0 let result = '' return new Promise((resolve, reject) => { rl.on('line', line => { if (n == count) { result = line rl.close() input.close() } n = n + 1 }) rl.on('close', () => { resolve(result) }) rl.on('error', reject) }) } export async function lineToLocation(fsPath: string, match: string, text?: string): Promise { let uri = URI.file(fsPath).toString() if (!fs.existsSync(fsPath)) return Location.create(uri, Range.create(0, 0, 0, 0)) const rl = readline.createInterface({ input: fs.createReadStream(fsPath, { encoding: 'utf8' }), }) let n = 0 let line = await new Promise(resolve => { let find = false rl.on('line', line => { if (line.includes(match)) { find = true rl.removeAllListeners() rl.close() resolve(line) return } n = n + 1 }) rl.on('close', () => { if (!find) resolve(undefined) }) }) if (line != null) { let character = text == null ? -1 : smartcaseIndex(text, line) if (character == -1) character = line.match(/^\s*/)[0].length let end = Position.create(n, character + (text ? text.length : 0)) return Location.create(uri, Range.create(Position.create(n, character), end)) } return Location.create(uri, Range.create(0, 0, 0, 0)) } export function sameFile(fullpath: string | null, other: string | null, caseInsensitive?: boolean): boolean { caseInsensitive = typeof caseInsensitive == 'boolean' ? caseInsensitive : platform.isWindows || platform.isMacintosh if (!fullpath || !other) return false fullpath = normalizeFilePath(fullpath) other = normalizeFilePath(other) if (caseInsensitive) return fullpath.toLowerCase() === other.toLowerCase() return fullpath === other } export function fileStartsWith(dir: string, pdir: string, caseInsensitive = platform.isWindows || platform.isMacintosh) { if (caseInsensitive) return dir.toLowerCase().startsWith(pdir.toLowerCase()) return dir.startsWith(pdir) } export async function writeFile(fullpath: string, content: string): Promise { await promisify(fs.writeFile)(fullpath, content, { encoding: 'utf8' }) } export function isFile(uri: string): boolean { return uri.startsWith('file:') } export function parentDirs(pth: string): string[] { let { root, dir } = path.parse(pth) if (dir === root) return [root] const dirs = [root] const parts = dir.slice(root.length).split(path.sep) for (let i = 1; i <= parts.length; i++) { dirs.push(path.join(root, parts.slice(0, i).join(path.sep))) } return dirs } export function normalizeFilePath(filepath: string) { return URI.file(path.resolve(path.normalize(filepath))).fsPath } export function isParentFolder(folder: string, filepath: string, checkEqual = false): boolean { let pdir = normalizeFilePath(folder) let dir = normalizeFilePath(filepath) if (sameFile(pdir, dir)) return checkEqual ? true : false return fileStartsWith(dir, pdir) && dir[pdir.length] == path.sep } ================================================ FILE: src/util/fuzzy.ts ================================================ 'use strict' import { ASCII_END } from './constants' export function getCharCodes(str: string): Uint16Array { let len = str.length let res = new Uint16Array(len) for (let i = 0, l = len; i < l; i++) { res[i] = str.charCodeAt(i) } return res } export function wordChar(ch: number): boolean { return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) } export function caseMatch(input: number, code: number, ignorecase = false): boolean { if (input === code) return true if (code < ASCII_END) { if (input >= 97 && input <= 122 && code + 32 === input) return true if (ignorecase) { if (input <= 90 && input + 32 === code) return true if (toLower(input) === code) return true } } else { let lower = toLower(code) if (lower === input || (ignorecase && toLower(input) === lower)) return true } return false } function toLower(code: number): number { return String.fromCharCode(code).toLowerCase().charCodeAt(0) } export function fuzzyChar(a: string, b: string, ignorecase = false): boolean { let ca = a.charCodeAt(0) let cb = b.charCodeAt(0) return caseMatch(ca, cb, ignorecase) } // upper case must match, lower case ignore case export function fuzzyMatch(needle: ArrayLike, text: string, ignorecase = false): boolean { let totalCount = needle.length let tl = text.length if (totalCount > tl) return false let i = 0 let curr = needle[0] for (let j = 0; j < tl; j++) { let code = text.charCodeAt(j) if (caseMatch(curr, code, ignorecase)) { i = i + 1 curr = needle[i] if (i === totalCount) return true continue } if (tl - j - 1 < totalCount - i) { break } } return false } ================================================ FILE: src/util/index.ts ================================================ 'use strict' import type { CancellationToken } from 'vscode-languageserver-protocol' import { crypto } from './node' export interface Disposable { dispose(): void } export function sha256(data: string): string { return crypto.createHash('sha256').update(data).digest('hex') } export function getConditionValue(value: T, testValue: T): T { return global.__TEST__ ? testValue : value } export const pariedCharacters: Map = new Map([ ['<', '>'], ['>', '<'], ['{', '}'], ['[', ']'], ['(', ')'], ]) export function defaultValue(val: T | undefined | null, defaultValue: T): T { return val == null ? defaultValue : val } export function wait(ms: number): Promise { if (ms <= 0) return Promise.resolve(undefined) return new Promise(resolve => { let timer = setTimeout(() => { resolve(undefined) }, ms) timer.unref() }) } export function waitWithToken(ms: number, token: CancellationToken): Promise { if (token.isCancellationRequested || !ms) return Promise.resolve(true) return new Promise(resolve => { let disposable = token.onCancellationRequested(() => { disposable.dispose() clearTimeout(timer) resolve(true) }) let timer = setTimeout(() => { disposable.dispose() resolve(false) }, ms) timer.unref() }) } export function waitNextTick(): Promise { return new Promise(resolve => { process.nextTick(() => { resolve(undefined) }) }) } export function waitImmediate(): Promise { return new Promise(resolve => { setImmediate(() => { resolve(undefined) }) }) } export function delay(func: () => void, defaultDelay: number): ((ms?: number) => void) & { clear: () => void } { let timer: NodeJS.Timeout let fn = (ms?: number) => { if (timer) clearTimeout(timer) timer = setTimeout(() => { func() }, ms ?? defaultDelay) timer.unref() } Object.defineProperty(fn, 'clear', { get: () => { return () => { clearTimeout(timer) } } }) return fn as any } export function concurrent(arr: T[], fn: (val: T) => Promise, limit = 3): Promise { if (arr.length == 0) return Promise.resolve() let finished = 0 let total = arr.length let remain = arr.slice() return new Promise(resolve => { let run = (val): void => { let cb = () => { finished = finished + 1 if (finished == total) { resolve() } else if (remain.length) { let next = remain.shift() run(next) } } fn(val).then(cb, cb) } for (let i = 0; i < Math.min(limit, remain.length); i++) { let val = remain.shift() run(val) } }) } export function disposeAll(disposables: Disposable[]): void { while (disposables.length) { const item = disposables.pop() item?.dispose() } } ================================================ FILE: src/util/is.ts ================================================ 'use strict' import { URL } from 'url' import { Command, CompletionItem, CompletionList, Hover, MarkedString, MarkupContent, Range } from 'vscode-languageserver-types' import { EditRange } from '../completion/types' /* eslint-disable id-blacklist */ const hasOwnProperty = Object.prototype.hasOwnProperty export function isUrl(url: any): boolean { try { new URL(url) return true } catch (e) { return false } } export function isHover(value: any): value is Hover { let candidate = value as Hover return !!candidate && objectLiteral(candidate) && ( MarkupContent.is(candidate.contents) || MarkedString.is(candidate.contents) || typedArray(candidate.contents, MarkedString.is) ) && ( value.range == null || Range.is(value.range) ) } export function isEditRange(value: any): value is EditRange { if (!value) return false if (Range.is(value)) return true return Range.is(value.insert) && Range.is(value.replace) } export function isCommand(obj: any): obj is Command { if (!obj || !string(obj.title) || !string(obj.command) || obj.command.length == 0) return false return true } export function isMarkdown(content: MarkupContent | string | undefined): boolean { if (content != null && content['kind'] == 'markdown') { return true } return false } export function isCompletionItem(obj: any): obj is CompletionItem { return obj && typeof obj.label === 'string' } export function isCompletionList(obj: any): obj is CompletionList { return !Array.isArray(obj) && Array.isArray(obj.items) } export function boolean(value: any): value is boolean { return typeof value === 'boolean' } export function string(value: any): value is string { return typeof value === 'string' } export function number(value: any): value is number { return typeof value === 'number' } export function array(array: any): array is any[] { return Array.isArray(array) } // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function func(value: any): value is Function { return typeof value == 'function' } export function objectLiteral(obj: any): obj is object { return ( obj != null && typeof obj === 'object' && !Array.isArray(obj) && !(obj instanceof RegExp) && !(obj instanceof Date) ) } export function emptyObject(obj: any): boolean { if (!objectLiteral(obj)) { return false } for (let key in obj) { if (hasOwnProperty.call(obj, key)) { return false } } return true } export function typedArray( value: any, check: (value: any) => boolean ): value is T[] { return Array.isArray(value) && (value as any).every(check) } ================================================ FILE: src/util/jsonRegistry.ts ================================================ 'use strict' import { Emitter, Event } from '../util/protocol' import type { IJSONSchema } from './jsonSchema' import { Registry } from './registry' export const Extensions = { JSONContribution: 'base.contributions.json' } export interface ISchemaContributions { schemas: { [id: string]: IJSONSchema } } export interface IJSONContributionRegistry { readonly onDidChangeSchema: Event /** * Register a schema to the registry. */ registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema): void /** * Notifies all listeners that the content of the given schema has changed. * @param uri The id of the schema */ notifySchemaChanged(uri: string): void /** * Get all schemas */ getSchemaContributions(): ISchemaContributions } class JSONContributionRegistry implements IJSONContributionRegistry { private schemasById: { [id: string]: IJSONSchema } private readonly _onDidChangeSchema = new Emitter() public readonly onDidChangeSchema: Event = this._onDidChangeSchema.event constructor() { this.schemasById = {} } public registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema): void { this.schemasById[uri] = unresolvedSchemaContent this._onDidChangeSchema.fire(uri) } public notifySchemaChanged(uri: string): void { this._onDidChangeSchema.fire(uri) } public getSchemaContributions(): ISchemaContributions { return { schemas: this.schemasById, } } } const jsonContributionRegistry = new JSONContributionRegistry() Registry.add(Extensions.JSONContribution, jsonContributionRegistry) ================================================ FILE: src/util/jsonSchema.ts ================================================ 'use strict' export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'null' | 'array' | 'object' export interface IJSONSchema { id?: string $id?: string $schema?: string type?: JSONSchemaType | JSONSchemaType[] title?: string default?: any definitions?: IJSONSchemaMap description?: string properties?: IJSONSchemaMap patternProperties?: IJSONSchemaMap additionalProperties?: boolean | IJSONSchema minProperties?: number maxProperties?: number dependencies?: IJSONSchemaMap | { [prop: string]: string[] } items?: IJSONSchema | IJSONSchema[] minItems?: number maxItems?: number uniqueItems?: boolean additionalItems?: boolean | IJSONSchema pattern?: string minLength?: number maxLength?: number minimum?: number maximum?: number exclusiveMinimum?: boolean | number exclusiveMaximum?: boolean | number multipleOf?: number required?: string[] $ref?: string anyOf?: IJSONSchema[] allOf?: IJSONSchema[] oneOf?: IJSONSchema[] not?: IJSONSchema enum?: any[] format?: string // schema draft 06 const?: any contains?: IJSONSchema propertyNames?: IJSONSchema examples?: any[] // schema draft 07 $comment?: string if?: IJSONSchema then?: IJSONSchema else?: IJSONSchema // schema 2019-09 unevaluatedProperties?: boolean | IJSONSchema unevaluatedItems?: boolean | IJSONSchema minContains?: number maxContains?: number deprecated?: boolean dependentRequired?: { [prop: string]: string[] } dependentSchemas?: IJSONSchemaMap $defs?: { [name: string]: IJSONSchema } $anchor?: string $recursiveRef?: string $recursiveAnchor?: string $vocabulary?: any // schema 2020-12 prefixItems?: IJSONSchema[] $dynamicRef?: string $dynamicAnchor?: string // VSCode extensions defaultSnippets?: IJSONSchemaSnippet[] errorMessage?: string patternErrorMessage?: string deprecationMessage?: string markdownDeprecationMessage?: string enumDescriptions?: string[] markdownEnumDescriptions?: string[] markdownDescription?: string doNotSuggest?: boolean suggestSortText?: string allowComments?: boolean allowTrailingCommas?: boolean } export interface IJSONSchemaMap { [name: string]: IJSONSchema } export interface IJSONSchemaSnippet { label?: string description?: string body?: any // a object that will be JSON stringified bodyText?: string // an already stringified JSON object that can contain new lines (\n) and tabs (\t) } ================================================ FILE: src/util/lodash.ts ================================================ 'use strict' /** Used for built-in method references. */ const objectProto = Object.prototype /** Used to check objects for own properties. */ const hasOwnProperty = objectProto.hasOwnProperty /** * Assigns own and inherited enumerable string keyed properties of source * objects to the destination object for all destination properties that * resolve to `undefined`. Source objects are applied from left to right. * Once a property is set, additional values of the same property are ignored. * * **Note:** This method mutates `object`. * @since 0.1.0 * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. * @see defaultsDeep * @example * * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }) * // => { 'a': 1, 'b': 2 } */ export function defaults(obj: any, ...sources: any[]): any { obj = Object(obj) sources.forEach(source => { if (source != null) { source = Object(source) for (const key in source) { const value = obj[key] if (value === undefined || (value === objectProto[key] && !hasOwnProperty.call(obj, key))) { obj[key] = source[key] } } } }) return obj } export function omit(obj: T, properties: string[]): T { let o = {} for (let key of Object.keys(obj)) { if (!properties.includes(key)) { o[key] = obj[key] } } return o as T } ================================================ FILE: src/util/map.ts ================================================ interface Item { previous: Item | undefined next: Item | undefined key: K value: V } export namespace Touch { export const None = 0 export const First = 1 export const AsOld = First export const Last = 2 export const AsNew: 2 = Last } // eslint-disable-next-line no-redeclare export type Touch = 0 | 1 | 2 export class LinkedMap implements Map { public readonly [Symbol.toStringTag] = 'LinkedMap' private _map: Map> private _head: Item | undefined private _tail: Item | undefined private _size: number private _state: number public constructor() { this._map = new Map>() this._head = undefined this._tail = undefined this._size = 0 this._state = 0 } public clear(): void { this._map.clear() this._head = undefined this._tail = undefined this._size = 0 this._state++ } public isEmpty(): boolean { return !this._head && !this._tail } public get size(): number { return this._size } public get first(): V | undefined { return this._head?.value } public get last(): V | undefined { return this._tail?.value } public before(key: K): V | undefined { const item = this._map.get(key) return item ? item.previous?.value : undefined } public after(key: K): V | undefined { const item = this._map.get(key) return item ? item.next?.value : undefined } public has(key: K): boolean { return this._map.has(key) } public get(key: K, touch: Touch = Touch.None): V | undefined { const item = this._map.get(key) if (!item) { return undefined } if (touch !== Touch.None) { this.touch(item, touch) } return item.value } public set(key: K, value: V, touch: Touch = Touch.None): this { let item = this._map.get(key) if (item) { item.value = value if (touch !== Touch.None) { this.touch(item, touch) } } else { item = { key, value, next: undefined, previous: undefined } switch (touch) { case Touch.None: this.addItemLast(item) break case Touch.First: this.addItemFirst(item) break case Touch.Last: this.addItemLast(item) break default: this.addItemLast(item) break } this._map.set(key, item) this._size++ } return this } public delete(key: K): boolean { return !!this.remove(key) } public remove(key: K): V | undefined { const item = this._map.get(key) if (!item) { return undefined } this._map.delete(key) this.removeItem(item) this._size-- return item.value } public shift(): V | undefined { if (!this._head && !this._tail) { return undefined } const item = this._head this._map.delete(item.key) this.removeItem(item) this._size-- return item.value } public forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { const state = this._state let current = this._head while (current) { if (thisArg) { callbackfn.bind(thisArg)(current.value, current.key, this) } else { callbackfn(current.value, current.key, this) } if (this._state !== state) { throw new Error(`LinkedMap got modified during iteration.`) } current = current.next } } public keys(): IterableIterator { const state = this._state let current = this._head const iterator: IterableIterator = { [Symbol.iterator]: () => { return iterator }, next: (): IteratorResult => { if (this._state !== state) { throw new Error(`LinkedMap got modified during iteration.`) } if (current) { const result = { value: current.key, done: false } current = current.next return result } else { return { value: undefined, done: true } } } } return iterator } public values(): IterableIterator { const state = this._state let current = this._head const iterator: IterableIterator = { [Symbol.iterator]: () => { return iterator }, next: (): IteratorResult => { if (this._state !== state) { throw new Error(`LinkedMap got modified during iteration.`) } if (current) { const result = { value: current.value, done: false } current = current.next return result } else { return { value: undefined, done: true } } } } return iterator } public entries(): IterableIterator<[K, V]> { const state = this._state let current = this._head const iterator: IterableIterator<[K, V]> = { [Symbol.iterator]: () => { return iterator }, next: (): IteratorResult<[K, V]> => { if (this._state !== state) { throw new Error(`LinkedMap got modified during iteration.`) } if (current) { const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false } current = current.next return result } else { return { value: undefined, done: true } } } } return iterator } public [Symbol.iterator](): IterableIterator<[K, V]> { return this.entries() } public trimOld(newSize: number): void { if (newSize >= this.size) { return } if (newSize === 0) { this.clear() return } let current = this._head let currentSize = this.size while (current && currentSize > newSize) { this._map.delete(current.key) current = current.next currentSize-- } this._head = current this._size = currentSize if (current) { current.previous = undefined } this._state++ } private addItemFirst(item: Item): void { // First time Insert if (!this._head && !this._tail) { this._tail = item } else { item.next = this._head this._head.previous = item } this._head = item this._state++ } private addItemLast(item: Item): void { // First time Insert if (!this._head && !this._tail) { this._head = item } else { item.previous = this._tail this._tail.next = item } this._tail = item this._state++ } private removeItem(item: Item): void { if (item === this._head && item === this._tail) { this._head = undefined this._tail = undefined } else if (item === this._head) { item.next.previous = undefined this._head = item.next } else if (item === this._tail) { item.previous.next = undefined this._tail = item.previous } else { const next = item.next const previous = item.previous next.previous = previous previous.next = next } item.next = undefined item.previous = undefined this._state++ } private touch(item: Item, touch: Touch): void { if ((touch !== Touch.First && touch !== Touch.Last)) { return } if (touch === Touch.First) { if (item === this._head) { return } const next = item.next const previous = item.previous // Unlink the item if (item === this._tail) { // previous must be defined since item was not head but is tail // So there are more than on item in the map previous!.next = undefined this._tail = previous } else { // Both next and previous are not undefined since item was neither head nor tail. next!.previous = previous previous!.next = next } // Insert the node at head item.previous = undefined item.next = this._head this._head.previous = item this._head = item this._state++ } else if (touch === Touch.Last) { if (item === this._tail) { return } const next = item.next const previous = item.previous // Unlink the item. if (item === this._head) { // next must be defined since item was not tail but is head // So there are more than on item in the map next!.previous = undefined this._head = next } else { // Both next and previous are not undefined since item was neither head nor tail. next!.previous = previous previous!.next = next } item.next = undefined item.previous = this._tail this._tail.next = item this._tail = item this._state++ } } public toJSON(): [K, V][] { const data: [K, V][] = [] this.forEach((value, key) => { data.push([key, value]) }) return data } public fromJSON(data: [K, V][]): void { this.clear() for (const [key, value] of data) { this.set(key, value) } } } export class LRUCache extends LinkedMap { private _limit: number private _ratio: number public constructor(limit: number, ratio = 1) { super() this._limit = limit this._ratio = Math.min(Math.max(0, ratio), 1) } public get limit(): number { return this._limit } public set limit(limit: number) { this._limit = limit this.checkTrim() } public get ratio(): number { return this._ratio } public set ratio(ratio: number) { this._ratio = Math.min(Math.max(0, ratio), 1) this.checkTrim() } public get(key: K, touch: Touch = Touch.AsNew): V | undefined { return super.get(key, touch) } public peek(key: K): V | undefined { return super.get(key, Touch.None) } public set(key: K, value: V): this { super.set(key, value, Touch.Last) this.checkTrim() return this } private checkTrim() { if (this.size > this._limit) { this.trimOld(Math.round(this._limit * this._ratio)) } } } ================================================ FILE: src/util/mutex.ts ================================================ 'use strict' export class Mutex { private tasks: (() => void)[] = [] private count = 1 private sched(): void { if (this.count > 0 && this.tasks.length > 0) { this.count-- let next = this.tasks.shift() next() } } public reset(): void { this.tasks = [] this.count = 1 } public get busy(): boolean { return this.count == 0 } public acquire(): Promise<() => void> { return new Promise<() => void>(res => { let task = () => { let released = false res(() => { if (!released) { released = true this.count++ this.sched() } }) } this.tasks.push(task) process.nextTick(this.sched.bind(this)) }) } public use(f: () => Promise): Promise { return this.acquire() .then(release => f() .then(res => { release() return res }) .catch(err => { release() throw err })) } } ================================================ FILE: src/util/node.ts ================================================ import type STYLES from 'ansi-styles' import type CHILD_PROCESS from 'child_process' import type CRYPTO from 'crypto' import type DEBOUNCE from 'debounce' import type FASTDIFF from 'fast-diff' import type FS from 'fs' import type * as GLOB from 'glob' import type * as Minimatch from 'minimatch' import type NET from 'net' import type OS from 'os' import type PATH from 'path' import type READLINE from 'readline' import type SEMVER from 'semver' import type STRIPANSI from 'strip-ansi' import type UNIDECODE from 'unidecode' import { inspect, promisify } from 'util' import type VM from 'vm' import type WHICH from 'which' export const fs = require('fs') as typeof FS export const path = require('path') as typeof PATH export const os = require('os') as typeof OS export const crypto = require('crypto') as typeof CRYPTO export const styles = require('ansi-styles') as typeof STYLES export const debounce = require('debounce') as typeof DEBOUNCE export const readline = require('readline') as typeof READLINE export const child_process = require('child_process') as typeof CHILD_PROCESS export const glob = require('glob') as typeof GLOB export const { minimatch } = require('minimatch') as typeof Minimatch export const which = require('which') as typeof WHICH export const semver = require('semver') as typeof SEMVER export const vm = require('vm') as typeof VM export const net = require('net') as typeof NET export const stripAnsi = require('strip-ansi') as typeof STRIPANSI export const fastDiff = require('fast-diff') as typeof FASTDIFF export const unidecode = require('unidecode') as typeof UNIDECODE export { inspect, promisify } ================================================ FILE: src/util/numbers.ts ================================================ 'use strict' import * as Is from './is' export function toNumber(n: number | undefined | null, defaultValue = 0): number { return Is.number(n) ? n : defaultValue } export function boolToNumber(val: boolean | undefined | null): number { return val ? 1 : 0 } export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max) } export function rot(index: number, modulo: number): number { return (modulo + (index % modulo)) % modulo } ================================================ FILE: src/util/object.ts ================================================ 'use strict' import * as Is from './is' export function isEmpty(obj: object | null | undefined): boolean { if (!obj) return true if (Array.isArray(obj)) return obj.length == 0 return Object.keys(obj).length == 0 } export function toObject(obj: T | null | undefined): Partial { return obj == null ? {} : obj } export function omitUndefined(obj: T): Partial { const result: any = {} Object.entries(obj).forEach(([key, val]) => { if (val !== undefined) result[key] = val }) return result } export function omitNullUndefined(obj: T): Partial { return Object.entries(obj).reduce((acc, [key, value]) => { if (value !== null && value !== undefined) { acc[key as keyof T] = value } return acc }, {} as Partial) } export function deepIterate(obj: object, fn: (node: object, key: string) => void): object { Object.entries(obj).forEach(([key, val]) => { fn(obj, key) if (Array.isArray(val)) { val.forEach(node => { if (Is.objectLiteral(node)) { deepIterate(node, fn) } }) } else if (Is.objectLiteral(val)) { deepIterate(val, fn) } }) return obj } export function toReadonly(obj: T): T { const result = {} for (let key of Object.keys(obj)) { Object.defineProperty(result, key, { value: obj[key], writable: false, enumerable: true }) } return result as T } export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { return obj } if (obj instanceof RegExp) { // See https://github.com/Microsoft/TypeScript/issues/10990 return obj as any } const result: any = Array.isArray(obj) ? [] : {} Object.keys(obj).forEach(key => { if (obj[key] && typeof obj[key] === 'object') { result[key] = deepClone(obj[key]) } else { result[key] = obj[key] } }) return result } const _hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwnProperty(obj: any, key: string): boolean { return _hasOwnProperty.call(obj, key) } export function deepFreeze(obj: T): T { if (!obj || typeof obj !== 'object') { return obj } const stack: any[] = [obj] while (stack.length > 0) { let obj = stack.shift() Object.freeze(obj) for (const key of Object.keys(obj)) { let prop = obj[key] if (typeof prop === 'object' && !Object.isFrozen(prop)) { stack.push(prop) } } } return obj } /** * Copies all properties of source into destination. The optional parameter "overwrite" allows to control * if existing properties on the destination should be overwritten or not. Defaults to true (overwrite). */ export function mixin( destination: any, source: any, overwrite = true ): any { if (!Is.objectLiteral(destination)) { return source } if (Is.objectLiteral(source)) { Object.keys(source).forEach(key => { if (key in destination) { if (overwrite) { if (Is.objectLiteral(destination[key]) && Is.objectLiteral(source[key])) { mixin(destination[key], source[key], overwrite) } else { destination[key] = source[key] } } } else { destination[key] = source[key] } }) } return destination } export function equals(one: any, other: any): boolean { if (one === other) { return true } if ( one === null || one === undefined || other === null || other === undefined ) { return false } if (typeof one !== typeof other) { return false } if (typeof one !== 'object') { return false } if (Array.isArray(one) !== Array.isArray(other)) { return false } let i: number let key: string if (Array.isArray(one)) { if (one.length !== other.length) { return false } for (i = 0; i < one.length; i++) { if (!equals(one[i], other[i])) { return false } } } else { const oneKeys: string[] = [] for (key in one) { oneKeys.push(key) } oneKeys.sort() const otherKeys: string[] = [] for (key in other) { otherKeys.push(key) } otherKeys.sort() if (!equals(oneKeys, otherKeys)) { return false } for (i = 0; i < oneKeys.length; i++) { if (!equals(one[oneKeys[i]], other[oneKeys[i]])) { return false } } } return true } ================================================ FILE: src/util/platform.ts ================================================ 'use strict' export interface IProcessEnvironment { [key: string]: string } interface INodeProcess { nextTick: () => void platform: string env: IProcessEnvironment getuid(): number } declare let process: INodeProcess export enum Platform { Web, Mac, Linux, Windows, Unknown } export function getPlatform(process: INodeProcess): Platform { let { platform } = process if (platform === 'win32') return Platform.Windows if (platform === 'darwin') return Platform.Mac if (platform === 'linux') return Platform.Linux return Platform.Unknown } let _platform: Platform = getPlatform(process) export const platform = _platform export const isWindows = _platform === Platform.Windows export const isMacintosh = _platform === Platform.Mac export const isLinux = _platform === Platform.Linux export const isNative = true export const isWeb = false ================================================ FILE: src/util/position.ts ================================================ 'use strict' import { Position, Range } from 'vscode-languageserver-types' export function rangeInRange(r: Range, range: Range): boolean { return positionInRange(r.start, range) === 0 && positionInRange(r.end, range) === 0 } export function equalsRange(r: Range, range: Range): boolean { if (!samePosition(r.start, range.start)) return false return samePosition(r.end, range.end) } export function samePosition(one: Position, two: Position): boolean { return one.line === two.line && one.character === two.character } export function adjacentPosition(pos: Position, range: Range) { return samePosition(pos, range.start) || samePosition(pos, range.end) } /** * A function that compares ranges, useful for sorting ranges * It will first compare ranges on the startPosition and then on the endPosition */ export function compareRangesUsingStarts(a: Range, b: Range): number { const aStartLineNumber = a.start.line | 0 const bStartLineNumber = b.start.line | 0 if (aStartLineNumber === bStartLineNumber) { const aStartColumn = a.start.character | 0 const bStartColumn = b.start.character | 0 if (aStartColumn === bStartColumn) { const aEndLineNumber = a.end.line | 0 const bEndLineNumber = b.end.line | 0 if (aEndLineNumber === bEndLineNumber) { const aEndColumn = a.end.character | 0 const bEndColumn = b.end.character | 0 return aEndColumn - bEndColumn } return aEndLineNumber - bEndLineNumber } return aStartColumn - bStartColumn } return aStartLineNumber - bStartLineNumber } /** * Convert to well formed range */ export function toValidRange(range: Range, max?: number): Range { let { start, end } = range if (start.line > end.line || (start.line === end.line && start.character > end.character)) { let m = start start = end end = m } start = Position.create(Math.max(0, start.line), Math.max(0, start.character)) let endCharacter = Math.max(0, end.character) if (typeof max === 'number' && endCharacter > max) endCharacter = max end = Position.create(Math.max(0, end.line), endCharacter) return { start, end } } export function rangeAdjacent(r: Range, range: Range): boolean { if (comparePosition(r.end, range.start) == 0) { return true } if (comparePosition(range.end, r.start) == 0) { return true } return false } /** * Check if two ranges have overlap character. */ export function rangeOverlap(r: Range, range: Range): boolean { let { start, end } = r if (comparePosition(end, range.start) <= 0) { return false } if (comparePosition(start, range.end) >= 0) { return false } return true } /** * Check if two ranges have overlap or nested */ export function rangeIntersect(r: Range, range: Range): boolean { if (positionInRange(r.start, range) == 0) { return true } if (positionInRange(r.end, range) == 0) { return true } if (rangeInRange(range, r)) { return true } return false } /** * Adjust from start position */ export function adjustRangePosition(range: Range, position: Position): Range { let { line, character } = position let { start, end } = range let endCharacter = end.line == start.line ? end.character + character : end.character return Range.create(start.line + line, character + start.character, end.line + line, endCharacter) } export function lineInRange(line: number, range: Range): boolean { let { start, end } = range return line >= start.line && line <= end.line } export function emptyRange(range: Range): boolean { let { start, end } = range return start.line == end.line && start.character == end.character } export function positionInRange(position: Position, range: Range): number { let { start, end } = range if (comparePosition(position, start) < 0) return -1 if (comparePosition(position, end) > 0) return 1 return 0 } export function comparePosition(position: Position, other: Position): number { if (position.line > other.line) return 1 if (other.line == position.line && position.character > other.character) return 1 if (other.line == position.line && position.character == other.character) return 0 return -1 } export function isSingleLine(range: Range): boolean { return range.start.line == range.end.line } /* * Get end position by content */ export function getEnd(start: Position, content: string): Position { const lines = content.split(/\r?\n/) const len = lines.length const lastLine = lines[len - 1] const end = len == 1 ? start.character + content.length : lastLine.length return Position.create(start.line + len - 1, end) } ================================================ FILE: src/util/processes.ts ================================================ 'use strict' import type { ChildProcess, ExecOptions } from 'child_process' import { pluginRoot } from './constants' import { CancellationError } from './errors' import { child_process, path, which } from './node' import { platform, Platform } from './platform' import iconv from 'iconv-lite' import { CancellationToken, Disposable } from './protocol' import { omit } from './lodash' export function isRunning(pid: number): boolean { try { let res: any = process.kill(pid, 0) return res == true } catch (e) { return e['code'] === 'EPERM' } } export function executable(command: string): boolean { try { which.sync(command) } catch (e) { return false } return true } export function runCommand(cmd: string, opts: ExecOptions & { encoding?: string } = {}, timeout?: CancellationToken | number, isWindows = platform === Platform.Windows): Promise { if (!isWindows) { opts.shell = opts.shell || process.env.SHELL } opts.maxBuffer = opts.maxBuffer ?? 500 * 1024 let encoding = opts.encoding || 'utf8' encoding = iconv.encodingExists(encoding) ? encoding : 'utf8' return new Promise((resolve, reject) => { let disposable: Disposable | undefined let cp: ChildProcess if (typeof timeout === 'number') { let timer = setTimeout(() => { terminate(cp) reject(new CancellationError()) }, timeout * 1000) disposable = Disposable.create(() => { clearTimeout(timer) }) } else if (CancellationToken.is(timeout)) { disposable = timeout.onCancellationRequested(() => { terminate(cp) reject(new CancellationError()) }) } cp = child_process.exec(cmd, { ...omit(opts, ['encoding']), encoding: 'buffer' }, (err, stdout, stderr) => { if (disposable) disposable.dispose() if (err) { reject(new Error(`exited with ${err.code}\n${err}\n${stderr.toString('utf8')}`)) return } resolve(iconv.decode(stdout, encoding)) }) }) } export function terminate(process: ChildProcess, cwd?: string, pt = platform): boolean { if (process.killed) return if (pt === Platform.Windows) { try { // This we run in Atom execFileSync is available. // Ignore stderr since this is otherwise piped to parent.stderr // which might be already closed. let options: any = { stdio: ['pipe', 'pipe', 'ignore'] } if (cwd) options.cwd = cwd child_process.execFileSync( 'taskkill', ['/T', '/F', '/PID', process.pid.toString()], options ) return true } catch (err) { return false } } else if (pt === Platform.Linux || pt === Platform.Mac) { try { let filepath = path.join(pluginRoot, 'bin/terminateProcess.sh') let result = child_process.spawnSync(filepath, [process.pid.toString()]) return result.error ? false : true } catch (err) { return false } } else { process.kill('SIGKILL') return true } } ================================================ FILE: src/util/protocol.ts ================================================ export { createProtocolConnection, generateRandomPipeName, InitializeRequest, ShutdownRequest, ExitNotification, LogMessageNotification, ShowMessageNotification, ShowMessageRequest, ShowDocumentRequest, PublishDiagnosticsNotification, RegistrationRequest, UnregistrationRequest, ApplyWorkspaceEditRequest, InitializedNotification, InlineCompletionItem, InlineCompletionContext, TraceFormat, ResourceOperationKind, FailureHandlingKind, LSPErrorCodes, MessageType, MessageReader, MessageWriter, Disposable, Event, CancellationToken, TextDocumentFilter, SignatureHelpTriggerKind, DocumentDiagnosticReportKind, DiagnosticServerCancellationData, RAL, Trace, FileOperationPatternKind, PositionEncodingKind, CompletionTriggerKind, TextDocumentSaveReason, UniquenessLevel, ErrorCodes, MonikerKind, IPCMessageReader, IPCMessageWriter, StreamMessageReader, StreamMessageWriter, ProgressType, ResponseError, ProtocolRequestType, ProtocolRequestType0, ProtocolNotificationType, ProtocolNotificationType0, RequestType, RequestType0, NotificationType, NotificationType0, Emitter, CancellationTokenSource, RelativePattern, WatchKind, FileChangeType, FoldingRangeKind, PrepareSupportDefaultBehavior, TokenFormat, TextDocumentSyncKind, TextDocumentRegistrationOptions, StaticRegistrationOptions, WorkDoneProgressOptions, WorkspaceSymbolRequest, WorkspaceSymbolResolveRequest, TypeHierarchyPrepareRequest, TypeHierarchySupertypesRequest, TypeHierarchySubtypesRequest, TypeDefinitionRequest, DidChangeWorkspaceFoldersNotification, WorkspaceFoldersRequest, WillSaveTextDocumentNotification, WillSaveTextDocumentWaitUntilRequest, SignatureHelpRequest, SemanticTokensRequest, SemanticTokensRangeRequest, SemanticTokensDeltaRequest, SemanticTokensRefreshRequest, SemanticTokensRegistrationType, SelectionRangeRequest, PrepareRenameRequest, HoverRequest, InlayHintRequest, InlayHintResolveRequest, InlayHintRefreshRequest, InlineValueRefreshRequest, InlineValueRequest, LinkedEditingRangeRequest, WorkDoneProgressCreateRequest, WorkDoneProgress, WorkDoneProgressCancelNotification, RenameRequest, ReferencesRequest, ImplementationRequest, DocumentFormattingRequest, DocumentRangeFormattingRequest, DocumentOnTypeFormattingRequest, FoldingRangeRequest, DidChangeWatchedFilesNotification, DidCreateFilesNotification, DidRenameFilesNotification, WillCreateFilesRequest, DidDeleteFilesNotification, WillRenameFilesRequest, WillDeleteFilesRequest, DocumentSymbolRequest, DocumentLinkRequest, DocumentLinkResolveRequest, DocumentDiagnosticRequest, WorkspaceDiagnosticRequest, DocumentHighlightRequest, DidOpenTextDocumentNotification, DidChangeTextDocumentNotification, DidSaveTextDocumentNotification, DidCloseTextDocumentNotification, DiagnosticRefreshRequest, CallHierarchyPrepareRequest, CallHierarchyIncomingCallsRequest, CallHierarchyOutgoingCallsRequest, CodeActionRequest, ExecuteCommandRequest, CodeActionResolveRequest, CodeLensResolveRequest, CodeLensRequest, CodeLensRefreshRequest, DocumentColorRequest, ColorPresentationRequest, CompletionRequest, CompletionResolveRequest, ConfigurationRequest, DidChangeConfigurationNotification, DeclarationRequest, DefinitionRequest, } from 'vscode-languageserver-protocol/node' ================================================ FILE: src/util/registry.ts ================================================ import type { IConfigurationPropertySchema } from '../configuration/registry' import { ConfigurationScope, IStringDictionary } from '../configuration/types' import { assert } from './errors' import { objectLiteral } from './is' import { deepClone, toObject } from './object' export interface IRegistry { /** * Adds the extension functions and properties defined by data to the * platform. The provided id must be unique. * @param id a unique identifier * @param data a contribution */ add(id: string, data: any): void /** * Returns true iff there is an extension with the provided id. * @param id an extension identifier */ knows(id: string): boolean /** * Returns the extension functions and properties defined by the specified key or null. * @param id an extension identifier */ as(id: string): T } class RegistryImpl implements IRegistry { private readonly data = new Map() public add(id: string, data: any): void { assert(typeof id === 'string') assert(objectLiteral(data)) assert(!this.data.has(id)) this.data.set(id, data) } public knows(id: string): boolean { return this.data.has(id) } public as(id: string): any { return this.data.get(id) || null } } export const Registry: IRegistry = new RegistryImpl() const sourcePrefixes = ['coc.source.', 'list.source.'] enum ScopeNames { Application = 'application', Window = 'window', Resource = 'resource', MachineOverridable = 'machine-overridable', LanguageOverridable = 'language-overridable', } function convertScope(key: string, scope: string, defaultScope: ConfigurationScope): ConfigurationScope { if (sourcePrefixes.some(p => key.startsWith(p))) return ConfigurationScope.APPLICATION if (scope === ScopeNames.Application) return ConfigurationScope.APPLICATION if (scope === ScopeNames.Window) return ConfigurationScope.WINDOW if (scope === ScopeNames.Resource || scope === ScopeNames.MachineOverridable) return ConfigurationScope.RESOURCE if (scope === ScopeNames.LanguageOverridable) return ConfigurationScope.LANGUAGE_OVERRIDABLE return defaultScope } /** * Properties to schema */ export function convertProperties(properties: object | null | undefined, defaultScope = ConfigurationScope.WINDOW): IStringDictionary { let obj: IStringDictionary = {} for (let [key, def] of Object.entries(toObject(properties))) { let data = deepClone(def) data.scope = convertScope(key, def.scope, defaultScope) obj[key] = data } return obj } ================================================ FILE: src/util/sequence.ts ================================================ export class Sequence { private _busy = false private _fns: (() => Promise)[] = [] private _resolves: (() => void)[] = [] public run(fn: () => Promise): void { if (!this._busy) { this._busy = true void fn().finally(() => { this.next() }) } else { this._fns.push(fn) } } public waitFinish(): Promise { if (!this._busy) return Promise.resolve() return new Promise(resolve => { this._resolves.push(resolve) }) } private next(): void { let fn = this._fns.shift() if (!fn) { this.finish() } else { void fn().finally(() => { this.next() }) } } private finish(): void { this._busy = false let fn: () => void while ((fn = this._resolves.pop()) != null) { fn() } } public cancel(): void { this._fns = [] this.finish() } } ================================================ FILE: src/util/string.ts ================================================ 'use strict' import type { Range } from 'vscode-languageserver-types' import { intable } from './array' import { CharCode } from './charCode' const UTF8_2BYTES_START = 0x80 const UTF8_3BYTES_START = 0x800 const UTF8_4BYTES_START = 65536 const encoding = 'utf8' const asciiTable: ReadonlyArray<[number, number]> = [ [48, 57], [65, 90], [97, 122] ] export function toErrorText(error: any): string { return error instanceof Error ? error.message : error.toString() } export function toInteger(text: string): number | undefined { let n = parseInt(text, 10) return isNaN(n) ? undefined : n } export function toText(text: string | number | null | undefined): string { if (typeof text === 'number') return text.toString() return text ?? '' } export function toBase64(text: string) { return global.Buffer.from(text).toString('base64') } export function isHighlightGroupCharCode(code: number): boolean { if (intable(code, asciiTable)) return true return code === CharCode.Underline || code === CharCode.Period || code === CharCode.AtSign } /** * A fast function (therefore imprecise) to check if code points are emojis. * Generated using https://github.com/alexdima/unicode-utils/blob/main/emoji-test.js */ export function isEmojiImprecise(x: number): boolean { return ( (x >= 0x1F1E6 && x <= 0x1F1FF) || (x === 8986) || (x === 8987) || (x === 9200) || (x === 9203) || (x >= 9728 && x <= 10175) || (x === 11088) || (x === 11093) || (x >= 127744 && x <= 128591) || (x >= 128640 && x <= 128764) || (x >= 128992 && x <= 129008) || (x >= 129280 && x <= 129535) || (x >= 129648 && x <= 129782) ) } /** * Get previous and after part of range */ export function rangeParts(text: string, range: Range): [string, string] { let { start, end } = range let lines = text.split(/\r?\n/) let before = '' let after = '' let len = lines.length // get start and end parts for (let i = 0; i < len; i++) { let curr = lines[i] if (i < start.line) { before += curr + '\n' continue } if (i > end.line) { after += curr + (i == len - 1 ? '' : '\n') continue } if (i == start.line) { before += curr.slice(0, start.character) } if (i == end.line) { after += curr.slice(end.character) + (i == len - 1 ? '' : '\n') } } return [before, after] } // lowerCase 1, upperCase 2 export function getCase(code: number): number { if (code >= 97 && code <= 122) return 1 if (code >= 65 && code <= 90) return 2 return 0 } export function getNextWord(codes: Uint16Array, index: number): [number, number] | undefined { let preCase = index == 0 ? 0 : getCase(codes[index - 1]) for (let i = index; i < codes.length; i++) { let curr = getCase(codes[i]) if (curr > 0 && curr != preCase) { return [i, codes[i]] } preCase = curr } return undefined } export function getCharIndexes(input: string, character: string): number[] { let res: number[] = [] for (let i = 0; i < input.length; i++) { if (input[i] == character) res.push(i) } return res } export function* iterateCharacter(input: string, character: string): Iterable { for (let i = 0; i < input.length; i++) { if (input[i] == character) yield i } } export function isHighSurrogate(codePoint: number): boolean { return codePoint >= 0xd800 && codePoint <= 0xdbff } export function isLowSurrogate(codePoint: number): boolean { return codePoint >= 0xdc00 && codePoint <= 0xdfff } /** * Get byte length from string, from code unit start index. */ export function byteLength(str: string, start = 0): number { if (start === 0) return Buffer.byteLength(str, encoding) let len = 0 let unitIndex = 0 for (let codePoint of str) { let n = codePoint.codePointAt(0) if (unitIndex >= start) { len += utf8_code2len(n) } unitIndex += (n >= UTF8_4BYTES_START ? 2 : 1) } return len } /** * utf16 code unit to byte index. */ export function byteIndex(content: string, index: number): number { let byteLength = 0 let codePoint: number | undefined let prevCodePoint: number | undefined let max = Math.min(index, content.length) for (let i = 0; i < max; i++) { codePoint = content.charCodeAt(i) if (isLowSurrogate(codePoint)) { if (prevCodePoint && isHighSurrogate(prevCodePoint)) { byteLength += 1 } else { byteLength += 3 } } else { byteLength += utf8_code2len(codePoint) } prevCodePoint = codePoint } return byteLength } export function upperFirst(str: string): string { return str?.length > 0 ? str[0].toUpperCase() + str.slice(1) : '' } export function indexOf(str: string, ch: string, count = 1): number { let curr = 0 for (let i = 0; i < str.length; i++) { if (str[i] == ch) { curr = curr + 1 if (curr == count) { return i } } } return -1 } export function characterIndex(content: string, byteIndex: number): number { if (byteIndex == 0) return 0 let characterIndex = 0 let total = 0 for (let codePoint of content) { let code = codePoint.codePointAt(0) if (code >= UTF8_4BYTES_START) { characterIndex += 2 total += 4 } else { characterIndex += 1 total += utf8_code2len(code) } if (total >= byteIndex) break } return characterIndex } export function utf8_code2len(code: number): number { if (code < UTF8_2BYTES_START) return 1 if (code < UTF8_3BYTES_START) return 2 if (code < UTF8_4BYTES_START) return 3 return 4 } /** * No need to create Buffer */ export function byteSlice(content: string, start: number, end?: number): string { let si = characterIndex(content, start) let ei = end === undefined ? undefined : characterIndex(content, end) return content.slice(si, ei) } export function isAlphabet(code: number): boolean { if (code >= 65 && code <= 90) return true if (code >= 97 && code <= 122) return true return false } export function doEqualsIgnoreCase(a: string, b: string, stopAt = a.length): boolean { if (typeof a !== 'string' || typeof b !== 'string') { return false } for (let i = 0; i < stopAt; i++) { const codeA = a.charCodeAt(i) const codeB = b.charCodeAt(i) if (codeA === codeB) { continue } // a-z A-Z if (isAlphabet(codeA) && isAlphabet(codeB)) { const diff = Math.abs(codeA - codeB) if (diff !== 0 && diff !== 32) { return false } } // Any other charcode else { if (String.fromCharCode(codeA).toLowerCase() !== String.fromCharCode(codeB).toLowerCase()) { return false } } } return true } export function equalsIgnoreCase(a: string, b: string): boolean { const len1 = a ? a.length : 0 const len2 = b ? b.length : 0 if (len1 !== len2) return false return doEqualsIgnoreCase(a, b) } export function contentToLines(content: string, eol: boolean): string[] { if (eol && content.endsWith('\n')) { return content.slice(0, -1).split('\n') } return content.split('\n') } function hasUpperCase(str: string): boolean { for (let i = 0, l = str.length; i < l; i++) { let code = str.charCodeAt(i) if (code >= 65 && code <= 90) { return true } } return false } function smartMatch(a: string, b: string): boolean { if (a === b) return true let c = b.charCodeAt(0) if (c >= 65 && c <= 90) { if (c + 32 === a.charCodeAt(0)) return true } return false } // check if string smartcase include the other string export function smartcaseIndex(input: string, other: string): number { if (input.length > other.length) return -1 if (input.length === 0) return 0 if (!hasUpperCase(input)) { return other.toLowerCase().indexOf(input) } let total = input.length let checked = 0 for (let i = 0; i < other.length; i++) { let ch = other[i] if (smartMatch(input[checked], ch)) { checked++ if (checked === total) { return i - checked + 1 } } else if (checked > 0) { i = i - checked checked = 0 } } return -1 } /** * For faster convert sequence utf16 character index to byte index */ export function bytes(text: string, max?: number): (characterIndex: number) => number { max = max ?? text.length let lens = new Uint8Array(max) let ascii = true let prevCodePoint: number | undefined for (let i = 0; i < max; i++) { let code = text.charCodeAt(i) let len: number if (isLowSurrogate(code)) { if (prevCodePoint && isHighSurrogate(prevCodePoint)) { len = 1 } else { len = 3 } } else { len = utf8_code2len(code) } if (ascii && len > 1) ascii = false lens[i] = len prevCodePoint = code } return characterIndex => { if (characterIndex === 0) return 0 if (ascii) return Math.min(characterIndex, max) let res = 0 for (let i = 0; i < Math.min(characterIndex, max); i++) { res += lens[i] } return res } } /** * Unicode class. */ export type UnicodeClass = | "ascii" | "punctuation" | "space" | "word" | "hiragana" | "katakana" | "cjkideograph" | "hangulsyllable" | "superscript" | "subscript" | "braille" | "other" // Unicode class ranges. This list is based on Neovim's classification. // reference: https://github.com/neovim/neovim/blob/052e048db676ef3e68efc497c02902e3d43e6255/src/nvim/mbyte.c#L1229-L1305 const nonAsciiUnicodeClassRanges = [ [0x037e, 0x037e, "punctuation"], [0x0387, 0x0387, "punctuation"], [0x055a, 0x055f, "punctuation"], [0x0589, 0x0589, "punctuation"], [0x05be, 0x05be, "punctuation"], [0x05c0, 0x05c0, "punctuation"], [0x05c3, 0x05c3, "punctuation"], [0x05f3, 0x05f4, "punctuation"], [0x060c, 0x060c, "punctuation"], [0x061b, 0x061b, "punctuation"], [0x061f, 0x061f, "punctuation"], [0x066a, 0x066d, "punctuation"], [0x06d4, 0x06d4, "punctuation"], [0x0700, 0x070d, "punctuation"], [0x0964, 0x0965, "punctuation"], [0x0970, 0x0970, "punctuation"], [0x0df4, 0x0df4, "punctuation"], [0x0e4f, 0x0e4f, "punctuation"], [0x0e5a, 0x0e5b, "punctuation"], [0x0f04, 0x0f12, "punctuation"], [0x0f3a, 0x0f3d, "punctuation"], [0x0f85, 0x0f85, "punctuation"], [0x104a, 0x104f, "punctuation"], [0x10fb, 0x10fb, "punctuation"], [0x1361, 0x1368, "punctuation"], [0x166d, 0x166e, "punctuation"], [0x1680, 0x1680, "space"], [0x169b, 0x169c, "punctuation"], [0x16eb, 0x16ed, "punctuation"], [0x1735, 0x1736, "punctuation"], [0x17d4, 0x17dc, "punctuation"], [0x1800, 0x180a, "punctuation"], [0x2000, 0x200b, "space"], [0x200c, 0x2027, "punctuation"], [0x2028, 0x2029, "space"], [0x202a, 0x202e, "punctuation"], [0x202f, 0x202f, "space"], [0x2030, 0x205e, "punctuation"], [0x205f, 0x205f, "space"], [0x2060, 0x27ff, "punctuation"], [0x2070, 0x207f, "superscript"], [0x2080, 0x2094, "subscript"], [0x20a0, 0x27ff, "punctuation"], [0x2800, 0x28ff, "braille"], [0x2900, 0x2998, "punctuation"], [0x29d8, 0x29db, "punctuation"], [0x29fc, 0x29fd, "punctuation"], [0x2e00, 0x2e7f, "punctuation"], [0x3000, 0x3000, "space"], [0x3001, 0x3020, "punctuation"], [0x3030, 0x3030, "punctuation"], [0x303d, 0x303d, "punctuation"], [0x3040, 0x309f, "hiragana"], [0x30a0, 0x30ff, "katakana"], [0x3300, 0x9fff, "cjkideograph"], [0xac00, 0xd7a3, "hangulsyllable"], [0xf900, 0xfaff, "cjkideograph"], [0xfd3e, 0xfd3f, "punctuation"], [0xfe30, 0xfe6b, "punctuation"], [0xff00, 0xff0f, "punctuation"], [0xff1a, 0xff20, "punctuation"], [0xff3b, 0xff40, "punctuation"], [0xff5b, 0xff65, "punctuation"], [0x1d000, 0x1d24f, "other"], [0x1d400, 0x1d7ff, "other"], [0x1f000, 0x1f2ff, "other"], [0x1f300, 0x1f9ff, "other"], [0x20000, 0x2a6df, "cjkideograph"], [0x2a700, 0x2b73f, "cjkideograph"], [0x2b740, 0x2b81f, "cjkideograph"], [0x2f800, 0x2fa1f, "cjkideograph"], ] as const /** * Get class of a Unicode character. */ export function getUnicodeClass(char: string): UnicodeClass { if (char == null || char.length === 0) return "other" const charCode = char.charCodeAt(0) // Check for ASCII character if (charCode <= 0x7f) { if (charCode === 0) return "other" if (/\s/.test(char)) return "space" if (/\w/.test(char)) return "word" return "punctuation" } for (const [start, end, category] of nonAsciiUnicodeClassRanges) { if (start <= charCode && charCode <= end) { return category } } return "other" } ================================================ FILE: src/util/textedit.ts ================================================ 'use strict' import { AnnotatedTextEdit, ChangeAnnotation, Position, Range, SnippetTextEdit, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-types' import { LinesTextDocument } from '../model/textdocument' import { DocumentChange } from '../types' import { isFalsyOrEmpty } from './array' import { diffLines } from './diff' import { equals, toObject } from './object' import { comparePosition, emptyRange, getEnd, samePosition, toValidRange } from './position' import { byteIndex, contentToLines, toText } from './string' export type TextChangeItem = [string[], number, number, number, number] export function getStartLine(edit: TextEdit): number { let { start, end } = edit.range if (edit.newText.endsWith('\n') && start.line == end.line && start.character == 0 && end.character == 0) { return start.line - 1 } return start.line } export function lineCountChange(edit: TextEdit): number { let { newText } = edit let range = getWellformedRange(edit.range) let n = range.end.line - range.start.line return newText.split(/\r?\n/).length - n - 1 } export function getWellformedRange(range: Range): Range { const start = range.start const end = range.end if (start.line > end.line || (start.line === end.line && start.character > end.character)) { return { start: end, end: start } } return range } export function getWellformedEdit(textEdit: TextEdit) { const range = getWellformedRange(textEdit.range) if (range !== textEdit.range) { return { newText: textEdit.newText, range } } return textEdit } function mergeSort(data: T[], compare: (a: T, b: T) => number): T[] { if (data.length <= 1) { // sorted return data } const p = (data.length / 2) | 0 const left = data.slice(0, p) const right = data.slice(p) mergeSort(left, compare) mergeSort(right, compare) let leftIdx = 0 let rightIdx = 0 let i = 0 while (leftIdx < left.length && rightIdx < right.length) { let ret = compare(left[leftIdx], right[rightIdx]) if (ret <= 0) { // smaller_equal -> take left to preserve order data[i++] = left[leftIdx++] } else { // greater -> take right data[i++] = right[rightIdx++] } } while (leftIdx < left.length) { data[i++] = left[leftIdx++] } while (rightIdx < right.length) { data[i++] = right[rightIdx++] } return data } export function mergeSortEdits(edits: T[]): T[] { return mergeSort(edits, (a, b) => { let diff = a.range.start.line - b.range.start.line if (diff === 0) { return a.range.start.character - b.range.start.character } return diff }) } export function emptyTextEdit(edit: TextEdit): boolean { return emptyRange(edit.range) && edit.newText.length === 0 } export function emptyWorkspaceEdit(edit: WorkspaceEdit): boolean { let { changes, documentChanges } = edit if (documentChanges && documentChanges.length) return false if (changes && Object.keys(changes).length) return false return true } export function getRangesFromEdit(uri: string, edit: WorkspaceEdit): Range[] | undefined { let { changes, documentChanges } = edit if (changes) { let edits = changes[uri] return edits ? edits.map(e => e.range) : undefined } else if (Array.isArray(documentChanges)) { for (let c of documentChanges) { if (TextDocumentEdit.is(c) && c.textDocument.uri == uri) { return c.edits.map(e => e.range) } } } return undefined } export function getConfirmAnnotations(changes: ReadonlyArray, changeAnnotations: { [id: string]: ChangeAnnotation }): ReadonlyArray { let keys: string[] = [] const add = (key: string | undefined) => { if (key && !keys.includes(key) && changeAnnotations[key]?.needsConfirmation) keys.push(key) } for (let change of changes) { if (TextDocumentEdit.is(change)) { change.edits.forEach(edit => { add(edit['annotationId']) }) } else { add(change.annotationId) } } return keys } export function isDeniedEdit(edit: TextEdit | AnnotatedTextEdit | SnippetTextEdit, denied: string[]): boolean { if (AnnotatedTextEdit.is(edit) && denied.includes(edit.annotationId)) return true return false } /** * Create new changes with denied filtered */ export function createFilteredChanges(documentChanges: DocumentChange[], denied: string[]): DocumentChange[] { let changes: DocumentChange[] = [] documentChanges.forEach(change => { if (TextDocumentEdit.is(change)) { let edits = change.edits.filter(edit => { return !isDeniedEdit(edit, denied) }) if (edits.length > 0) { changes.push({ textDocument: change.textDocument, edits }) } } else if (!denied.includes(change.annotationId)) { changes.push(change) } }) return changes } export function getAnnotationKey(change: DocumentChange): string | undefined { let key: string if (TextDocumentEdit.is(change)) { if (AnnotatedTextEdit.is(change.edits[0])) { key = change.edits[0].annotationId } } else { key = change.annotationId } return key } export function toDocumentChanges(edit: WorkspaceEdit): DocumentChange[] { if (edit.documentChanges) return edit.documentChanges let changes: DocumentChange[] = [] for (let [uri, edits] of Object.entries(toObject(edit.changes))) { changes.push({ textDocument: { uri, version: null }, edits }) } return changes } /** * Filter unnecessary edits and fix edits. */ export function filterSortEdits(textDocument: LinesTextDocument, edits: TextEdit[]): TextEdit[] { let res: TextEdit[] = [] let end = textDocument.end let checkEnd = end.line > 0 && end.character == 0 let prevDelete: Position | undefined for (let i = 0; i < edits.length; i++) { let edit = edits[i] let { newText, range } = edit let max = (textDocument.lines[range.end.line] ?? '').length range = toValidRange(edit.range, max) if (prevDelete) { // merge possible delete, insert edits. if (samePosition(prevDelete, range.start) && emptyRange(range) && newText.length > 0) { let last = res[res.length - 1] last.newText = newText prevDelete = undefined continue } prevDelete = undefined } if (newText.includes('\r')) newText = newText.replace(/\r\n/g, '\n') let d = comparePosition(range.end, end) if (d > 0) range.end = { line: end.line, character: end.character } if (textDocument.getText(range) !== newText) { // Adjust textEdit to make it acceptable by nvim_buf_set_text if (d === 0 && checkEnd && !emptyRange(range) && newText.endsWith('\n')) { newText = newText.slice(0, -1) let text = textDocument.lines[end.line - 1] range.end = Position.create(end.line - 1, text.length) } else if (newText.length == 0) { prevDelete = range.start } res.push({ range, newText }) } } return mergeSortEdits(res) } /** * Apply valid & sorted edits */ export function applyEdits(document: LinesTextDocument, edits: TextEdit[] | undefined): string[] | undefined { if (isFalsyOrEmpty(edits)) return undefined if (edits.length == 1) { let { start, end } = edits[0].range let { lines } = document let sl = lines[start.line] ?? '' let el = lines[end.line] ?? '' let content = sl.substring(0, start.character) + edits[0].newText + el.substring(end.character) if (end.line >= lines.length && document.eol) { if (content == '') { const result = [...lines.slice(0, start.line)] return result.length === 0 ? [''] : result } if (content.endsWith('\n')) content = content.slice(0, -1) return [...lines.slice(0, start.line), ...content.split('\n')] } return [...lines.slice(0, start.line), ...content.split('\n'), ...lines.slice(end.line + 1)] } let text = document.getText() let lastModifiedOffset = 0 const spans = [] for (const e of edits) { let startOffset = document.offsetAt(e.range.start) if (startOffset < lastModifiedOffset) { throw new Error('Overlapping edit') } else if (startOffset > lastModifiedOffset) { spans.push(text.substring(lastModifiedOffset, startOffset)) } if (e.newText.length) { spans.push(e.newText) } lastModifiedOffset = document.offsetAt(e.range.end) } spans.push(text.substring(lastModifiedOffset)) let result = spans.join('') if (result === text) return undefined return contentToLines(result, document.eol) } export function getRangeText(lines: ReadonlyArray, range: Range): string { let result: string[] = [] const { start, end } = range if (start.line === end.line) { let line = toText(lines[start.line]) return line.slice(start.character, end.character) } for (let i = start.line; i <= end.line; i++) { let line = toText(lines[i]) let text = line if (i === start.line) { text = line.slice(start.character) } else if (i === end.line) { text = line.slice(0, end.character) } result.push(text) } return result.join('\n') } export function validEdit(edit: TextEdit): boolean { let { range, newText } = edit if (!newText.endsWith('\n')) return false if (range.end.character !== 0) return false return true } export function toTextChanges(lines: ReadonlyArray, edits: TextEdit[]): TextChangeItem[] { if (edits.length === 0) return [] for (let edit of edits) { if (edit.range.end.line > lines.length) return [] if (edit.range.end.line == lines.length) { // should only be insert at the end if (!validEdit(edit)) return [] let line = lines.length - 1 let character = lines[line].length if (emptyRange(edit.range)) { // convert to insert at the end of last line. edit.range = Range.create(line, character, line, character) edit.newText = '\n' + edit.newText.slice(0, -1) } else { // convert to replace to the end of last line. const start = edit.range.start edit.range = Range.create(start, Position.create(line, character)) edit.newText = edit.newText.slice(0, -1) } } } return edits.map(o => { const oldText = getRangeText(lines, o.range) let edit = reduceTextEdit(o, oldText) let { start, end } = edit.range let sl = toText(lines[start.line]) let sc = byteIndex(sl, start.character) let el = end.line == start.line ? sl : toText(lines[end.line]) let ec = byteIndex(el, end.character) let { newText } = edit return [newText.length > 0 ? newText.split('\n') : [], start.line, sc, end.line, ec] }) } export function getChangedPosition(start: Position, edit: TextEdit): { line: number; character: number } { let { range, newText } = edit if (comparePosition(range.end, start) <= 0) { let lines = newText.split('\n') let lineCount = lines.length - (range.end.line - range.start.line) - 1 let character = start.character if (range.end.line == start.line) { let last = lines[lines.length - 1].length if (lines.length > 1) { character = last + character - range.end.character } else { character = range.start.character + last + character - range.end.character } } return { line: lineCount, character: character - start.character } } return { line: 0, character: 0 } } export function getPosition(start: Position, edit: TextEdit): Position { let { line, character } = start let { range, newText } = edit let { end } = range let lines = newText.split('\n') let lineCount = lines.length - (end.line - range.start.line) - 1 let c = range.end.line - start.line if (c > 0) return { line, character } if (c < 0) return { line: line + lineCount, character } if (lines.length > 1) { let last = lines[lines.length - 1].length return { line: line + lineCount, character: last + character - end.character } } let d = range.start.character - range.end.character return { line: line + lineCount, character: d + newText.length + character } } /** * Get new position from sorted edits */ export function getPositionFromEdits(start: Position, edits: TextEdit[]): Position { let position = Position.create(start.line, start.character) let before = false for (let i = edits.length - 1; i >= 0; i--) { let edit = edits[i] if (before) { position.line += lineCountChange(edit) continue } let d = comparePosition(edit.range.end, position) if (d > 0) continue if (edit.range.end.line == position.line) { position = getPosition(position, edit) } else { before = true position.line += lineCountChange(edit) } } return position } export function getChangedLineCount(start: Position, edits: TextEdit[]): number { let total = 0 for (let edit of edits) { let r = getWellformedRange(edit.range) if (comparePosition(r.end, start) <= 0) { total += lineCountChange(edit) } } return total } /** * Merge sorted edits to single textedit */ export function mergeTextEdits(edits: TextEdit[], oldLines: ReadonlyArray, newLines: ReadonlyArray): TextEdit { let start = edits[0].range.start let end = edits[edits.length - 1].range.end let lr = oldLines.length - end.line let cr = (oldLines[end.line] ?? '').length - end.character let line = newLines.length - lr let character = (newLines[line] ?? '').length - cr let newText = getRangeText(newLines, Range.create(start, Position.create(line, character))) return TextEdit.replace(Range.create(start, end), newText) } /* * Avoid change unnecessary range of text. */ export function reduceTextEdit(edit: TextEdit, oldText: string): TextEdit { if (oldText.length === 0) return edit let { range, newText } = edit let ol = oldText.length let nl = newText.length if (ol === 0 || nl === 0) return edit let { start, end } = range let bo = 0 for (let i = 1; i <= Math.min(nl, ol); i++) { if (newText[i - 1] === oldText[i - 1]) { bo = i } else { break } } let eo = 0 let t = Math.min(nl - bo, ol - bo) if (t > 0) { for (let i = 1; i <= t; i++) { if (newText[nl - i] === oldText[ol - i]) { eo = i } else { break } } } let text = eo == 0 ? newText.slice(bo) : newText.slice(bo, -eo) if (bo > 0) start = getEnd(start, newText.slice(0, bo)) if (eo > 0) end = getEnd(range.start, oldText.slice(0, -eo)) return TextEdit.replace(Range.create(start, end), text) } export function getRevertEdit(oldLines: ReadonlyArray, newLines: ReadonlyArray, startLine: number): TextEdit | undefined { if (equals(oldLines, newLines)) return undefined let changed = diffLines(oldLines, newLines, startLine) let original = oldLines.slice(changed.start, changed.end) let range = Range.create(changed.start, 0, changed.start + changed.replacement.length, 0) return TextEdit.replace(range, original.join('\n') + (original.length > 0 ? '\n' : '')) } ================================================ FILE: src/util/timing.ts ================================================ 'use strict' import { createLogger } from '../logger' const logger = createLogger('timing') interface Timing { start(label?: string): void stop(): void } /** * Trace the duration and show error on timeout */ export function createTiming(name: string, timeout?: number): Timing { let start: number let timer: NodeJS.Timeout let _label: string return { start(label?: string) { _label = label start = Date.now() clearTimeout(timer) if (timeout) { timer = setTimeout(() => { logger.error(`${name} timeout after ${timeout}ms`) }, timeout) timer.unref() } }, stop() { clearTimeout(timer) logger.trace(`${name}${_label ? ` ${_label}` : ''} cost:`, Date.now() - start) } } } ================================================ FILE: src/window.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import type { Position, Range } from 'vscode-languageserver-types' import type { WorkspaceConfiguration } from './configuration/types' import channels from './core/channels' import { Dialogs, InputOptions, Item, MenuOption, QuickPickConfig, QuickPickOptions } from './core/dialogs' import type { TextEditor } from './core/editors' import { HighlightDiff, Highlights } from './core/highlights' import { Notifications, ProgressOptions } from './core/notifications' import Terminals, { OpenTerminalOption, TerminalResult } from './core/terminals' import * as ui from './core/ui' import type Cursors from './cursors/index' import type { Dialog, DialogConfig } from './model/dialog' import type { FloatWinConfig } from './model/floatFactory' import InputBox, { InputPreference } from './model/input' import type { MenuItem } from './model/menu' import type { MessageItem, NotificationConfig } from './model/notification' import type { Progress } from './model/progress' import type QuickPick from './model/quickpick' import type { StatusBarItem } from './model/status' import type { TerminalModel, TerminalOptions } from './model/terminal' import type { TreeView, TreeViewOptions } from './tree' import type { FloatConfig, FloatFactory, HighlightItem, OutputChannel, QuickPickItem } from './types' import { toObject } from './util/object' import { CancellationToken, Event } from './util/protocol' import type { Workspace } from './workspace' export interface StatusItemOption { progress?: boolean } export class Window { private nvim: Neovim public highlights: Highlights = new Highlights() private terminalManager: Terminals = new Terminals() public readonly cursors: Cursors public readonly dialogs = new Dialogs() public readonly notifications = new Notifications(this.dialogs) private workspace: Workspace constructor() { Object.defineProperty(this.highlights, 'nvim', { get: () => this.nvim }) Object.defineProperty(this.dialogs, 'nvim', { get: () => this.nvim }) Object.defineProperty(this.dialogs, 'configuration', { get: () => this.workspace.initialConfiguration }) Object.defineProperty(this.notifications, 'nvim', { get: () => this.nvim }) Object.defineProperty(this.notifications, 'configuration', { get: () => this.workspace.initialConfiguration }) Object.defineProperty(this.notifications, 'statusLine', { get: () => this.workspace.statusLine }) } public get activeTextEditor(): TextEditor | undefined { return this.workspace.editors.activeTextEditor } public get visibleTextEditors(): TextEditor[] { return this.workspace.editors.visibleTextEditors } public get onDidTabClose(): Event { return this.workspace.editors.onDidTabClose } public get onDidChangeActiveTextEditor(): Event { return this.workspace.editors.onDidChangeActiveTextEditor } public get onDidChangeVisibleTextEditors(): Event> { return this.workspace.editors.onDidChangeVisibleTextEditors } public get terminals(): ReadonlyArray { return this.terminalManager.terminals } public get onDidOpenTerminal(): Event { return this.terminalManager.onDidOpenTerminal } public get onDidCloseTerminal(): Event { return this.terminalManager.onDidCloseTerminal } public async createTerminal(opts: TerminalOptions): Promise { return await this.terminalManager.createTerminal(this.nvim, opts) } /** * Run command in vim terminal for result * @param cmd Command to run. * @param cwd Cwd of terminal, default to result of |getcwd()|. */ public async runTerminalCommand(cmd: string, cwd?: string, keepfocus = false): Promise { return await this.terminalManager.runTerminalCommand(this.nvim, cmd, cwd, keepfocus) } /** * Open terminal window. * @param cmd Command to run. * @param opts Terminal option. * @returns number buffer number of terminal */ public async openTerminal(cmd: string, opts?: OpenTerminalOption): Promise { return await this.terminalManager.openTerminal(this.nvim, cmd, opts) } /** * Reveal message with message type. * @param msg Message text to show. * @param messageType Type of message, could be `error` `warning` and `more`, default to `more` */ public showMessage(msg: string, messageType: ui.MsgTypes = 'more'): void { this.notifications.echoMessages(msg, messageType) } /** * Create a new output channel * @param name Unique name of output channel. * @returns A new output channel. */ public createOutputChannel(name: string): OutputChannel { return channels.create(name, this.nvim) } /** * Reveal buffer of output channel. * @param name Name of output channel. * @param cmd command for open output channel. * @param preserveFocus Preserve window focus when true. */ public showOutputChannel(name: string, cmd?: string, preserveFocus?: boolean): void { let command = cmd ? cmd : this.configuration.get('workspace.openOutputCommand', 'vs') channels.show(name, command, preserveFocus) } /** * Echo lines at the bottom of vim. * @param lines Line list. * @param truncate Truncate the lines to avoid 'press enter to continue' when true */ public async echoLines(lines: string[], truncate = false): Promise { await ui.echoLines(this.nvim, this.workspace.env, lines, truncate) } /** * Get current cursor position (line, character both 0 based). * @returns Cursor position. */ public getCursorPosition(): Promise { return ui.getCursorPosition(this.nvim) } /** * Move cursor to position. * @param position LSP position. */ public async moveTo(position: Position): Promise { await ui.moveTo(this.nvim, position, this.workspace.env.isVim) } /** * Get selected range for current document */ public getSelectedRange(mode: string): Promise { return ui.getSelection(this.nvim, mode) } /** * Visual select range of current document */ public async selectRange(range: Range): Promise { await ui.selectRange(this.nvim, range, this.nvim.isVim) } /** * Get current cursor character offset in document, * length of line break would always be 1. * @returns Character offset. */ public getOffset(): Promise { return ui.getOffset(this.nvim) } /** * Get screen position of current cursor(relative to editor), * both `row` and `col` are 0 based. * @returns Cursor screen position. */ public getCursorScreenPosition(): Promise { return ui.getCursorScreenPosition(this.nvim) } /** * Create a {@link TreeView} instance. * @param viewId Id of the view, used as title of TreeView when title doesn't exist. * @param options Options for creating the {@link TreeView} * @returns a {@link TreeView}. */ public createTreeView(viewId: string, options: TreeViewOptions): TreeView { const BasicTreeView = require('./tree/TreeView').default return new BasicTreeView(viewId, options) } /** * Create statusbar item that would be included in `g:coc_status`. * @param priority Higher priority item would be shown right. * @param option * @return A new status bar item. */ public createStatusBarItem(priority = 0, option: StatusItemOption = {}): StatusBarItem { return this.workspace.statusLine.createStatusBarItem(priority, option.progress) } /** * Get diff from highlight items and current highlights on vim. * Return null when buffer not loaded * @param bufnr Buffer number * @param ns Highlight namespace * @param items Highlight items * @param region 0 based start and end line count (end inclusive) * @param token CancellationToken * @returns {Promise} */ public async diffHighlights(bufnr: number, ns: string, items: HighlightItem[], region?: [number, number], token?: CancellationToken): Promise { return this.highlights.diffHighlights(bufnr, ns, items, region, token) } /** * Create a FloatFactory, user's configurations are respected. * @param {FloatWinConfig} conf - Float window configuration * @returns {FloatFactory} */ public createFloatFactory(conf: FloatWinConfig): FloatFactory { let configuration = this.workspace.initialConfiguration let defaults = toObject(configuration.get('floatFactory.floatConfig')) as FloatConfig let markdownPreference = this.workspace.configurations.markdownPreference return ui.createFloatFactory(this.workspace.nvim, Object.assign({ ...markdownPreference, maxWidth: 80 }, conf), defaults) } /** * Show quickpick for single item, use `window.menuPick` for menu at current current position. * @deprecated Use 'window.showMenuPicker()' or `window.showQuickPick` instead. * @param items Label list. * @param placeholder Prompt text, default to 'choose by number'. * @returns Index of selected item, or -1 when canceled. */ public async showQuickpick(items: string[], placeholder = 'Choose by number'): Promise { return await this.showMenuPicker(items, { title: placeholder, position: 'center' }) } /** * Shows a selection list. */ public async showQuickPick(itemsOrItemsPromise: Item[] | Promise, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Promise { return await this.dialogs.showQuickPick(itemsOrItemsPromise, options, token) } /** * Creates a {@link QuickPick} to let the user pick an item or items from a * list of items of type T. * * Note that in many cases the more convenient {@link window.showQuickPick} * is easier to use. {@link window.createQuickPick} should be used * when {@link window.showQuickPick} does not offer the required flexibility. * @return A new {@link QuickPick}. */ public async createQuickPick(config: QuickPickConfig = {}): Promise> { return await this.dialogs.createQuickPick(config) } public async requestInputList(prompt: string, items: string[]): Promise { if (items.length > this.workspace.env.lines) { items = items.slice(0, this.workspace.env.lines - 2) } return await this.dialogs.requestInputList(prompt, items) } /** * Show menu picker at current cursor position. * @param items Array of texts. * @param option Options for menu. * @param token A token that can be used to signal cancellation. * @returns Selected index (0 based), -1 when canceled. */ public async showMenuPicker(items: string[] | MenuItem[], option?: MenuOption, token?: CancellationToken): Promise { return await this.dialogs.showMenuPicker(items, option, token) } /** * Prompt user for confirm, a float/popup window would be used when possible, * use vim's |confirm()| function as callback. * @param title The prompt text. * @returns Result of confirm. */ public async showPrompt(title: string): Promise { return await this.dialogs.showPrompt(title) } /** * Show dialog window at the center of screen. * Note that the dialog would always be closed after button click. * @param config Dialog configuration. * @returns Dialog or null when dialog can't work. */ public async showDialog(config: DialogConfig): Promise

{ return await this.dialogs.showDialog(config) } /** * Request input from user * @param title Title text of prompt window. * @param value Default value of input, empty text by default. * @param {InputOptions} option for input window * @returns {Promise} */ public async requestInput(title: string, value?: string, option?: InputOptions): Promise { return await this.dialogs.requestInput(title, this.workspace.env, value, option) } /** * Creates and show a {@link InputBox} to let the user enter some text input. * @return A new {@link InputBox}. */ public async createInputBox(title: string, value: string | undefined, option?: InputPreference): Promise { return await this.dialogs.createInputBox(title, value, option) } /** * Show multiple picker at center of screen. * Use `this.workspace.env.dialog` to check if dialog could work. * @param items Array of QuickPickItem or string. * @param title Title of picker dialog. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ public async showPickerDialog(items: string[], title: string, token?: CancellationToken): Promise public async showPickerDialog(items: T[], title: string, token?: CancellationToken): Promise public async showPickerDialog(items: any, title: string, token?: CancellationToken): Promise { return await this.dialogs.showPickerDialog(items, title, token) } /** * Show an information message to users. Optionally provide an array of items which will be presented as * clickable buttons. * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ public async showInformationMessage(message: string, ...items: T[]): Promise { return await this.notifications._showMessage('Info', message, items) } /** * Show an warning message to users. Optionally provide an array of items which will be presented as * clickable buttons. * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ public async showWarningMessage(message: string, ...items: T[]): Promise { return await this.notifications._showMessage('Warning', message, items) } /** * Show an error message to users. Optionally provide an array of items which will be presented as * clickable buttons. * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ public async showErrorMessage(message: string, ...items: T[]): Promise { return await this.notifications._showMessage('Error', message, items) } public async showNotification(config: NotificationConfig): Promise { let stack = Error().stack await this.notifications.showNotification(config, stack) } /** * Show progress in the editor. Progress is shown while running the given callback * and while the promise it returned isn't resolved nor rejected. */ public async withProgress(options: ProgressOptions, task: (progress: Progress, token: CancellationToken) => Thenable): Promise { return this.notifications.withProgress(options, task) } /** * Apply highlight diffs, normally used with `window.diffHighlights` * * Timer is used to add highlights when there're too many highlight items to add, * the highlight process won't be finished on that case. * @param {number} bufnr - Buffer name * @param {string} ns - Namespace * @param {number} priority * @param {HighlightDiff} diff * @param {boolean} notify - Use notification, default false. * @returns {Promise} */ public async applyDiffHighlights(bufnr: number, ns: string, priority: number, diff: HighlightDiff, notify = false): Promise { return this.highlights.applyDiffHighlights(bufnr, ns, priority, diff, notify) } /** * Get visible ranges of bufnr with optional winid */ public async getVisibleRanges(bufnr: number, winid?: number): Promise<[number, number][]> { return await ui.getVisibleRanges(this.nvim, bufnr, winid) } private get configuration(): WorkspaceConfiguration { return this.workspace.initialConfiguration } public dispose(): void { this.terminalManager.dispose() } } export default new Window() ================================================ FILE: src/workspace.ts ================================================ 'use strict' import { Neovim } from '@chemzqm/neovim' import type { DocumentFilter, DocumentSelector, WorkspaceFoldersChangeEvent } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { CreateFileOptions, DeleteFileOptions, FormattingOptions, Location, LocationLink, Position, Range, RenameFileOptions, WorkspaceEdit, WorkspaceFolder } from 'vscode-languageserver-types' import { URI } from 'vscode-uri' import Configurations from './configuration' import ConfigurationShape from './configuration/shape' import type { ConfigurationResourceScope, WorkspaceConfiguration } from './configuration/types' import Autocmds, { AutocmdOptionWithStack } from './core/autocmds' import channels from './core/channels' import ContentProvider from './core/contentProvider' import Documents from './core/documents' import Editors from './core/editors' import { FileSystemWatcher, FileSystemWatcherManager } from './core/fileSystemWatcher' import Files, { FileCreateEvent, FileDeleteEvent, FileRenameEvent, FileWillCreateEvent, FileWillDeleteEvent, FileWillRenameEvent, TextDocumentWillSaveEvent } from './core/files' import { callAsync, createNameSpace, findUp, getWatchmanPath, has, resolveModule, score } from './core/funcs' import Keymaps, { KeymapCallback, LocalMode, MapMode } from './core/keymaps' import * as ui from './core/ui' import Watchers from './core/watchers' import WorkspaceFolderController from './core/workspaceFolder' import events from './events' import { createLogger } from './logger' import BufferSync, { SyncItem } from './model/bufferSync' import DB from './model/db' import type Document from './model/document' import { FuzzyMatch, FuzzyWasi, initFuzzyWasm } from './model/fuzzyMatch' import Mru from './model/mru' import StatusLine from './model/status' import { StrWidth } from './model/strwidth' import TabsModel from './model/tabs' import Task from './model/task' import { LinesTextDocument } from './model/textdocument' import { TextDocumentContentProvider } from './provider' import { Autocmd, DidChangeTextDocumentParams, Env, FileWatchConfig, GlobPattern, IConfigurationChangeEvent, KeymapOption, LocationWithTarget, QuickfixItem, TextDocumentMatch } from './types' import { defaultValue, getConditionValue } from './util' import { APIVERSION, VERSION, dataHome, pluginRoot, userConfigFile } from './util/constants' import { onUnexpectedError } from './util/errors' import { FileType, getFileType } from './util/fs' import { IJSONSchema } from './util/jsonSchema' import { path } from './util/node' import { toObject } from './util/object' import { runCommand } from './util/processes' import { CancellationToken, Disposable, Emitter, Event } from './util/protocol' const logger = createLogger('workspace') const methods = [ 'showMessage', 'runTerminalCommand', 'openTerminal', 'showQuickpick', 'menuPick', 'openLocalConfig', 'showPrompt', 'createStatusBarItem', 'createOutputChannel', 'showOutputChannel', 'requestInput', 'echoLines', 'getCursorPosition', 'moveTo', 'getOffset', 'getSelectedRange', 'selectRange', 'createTerminal', ] export class Workspace { public readonly onDidChangeConfiguration: Event public readonly onDidOpenTextDocument: Event public readonly onDidCloseTextDocument: Event public readonly onDidChangeTextDocument: Event public readonly onDidSaveTextDocument: Event public readonly onWillSaveTextDocument: Event public readonly onDidChangeWorkspaceFolders: Event public readonly onDidCreateFiles: Event public readonly onDidRenameFiles: Event public readonly onDidDeleteFiles: Event public readonly onWillCreateFiles: Event public readonly onWillRenameFiles: Event public readonly onWillDeleteFiles: Event public readonly nvim: Neovim public readonly configurations: Configurations public readonly workspaceFolderControl: WorkspaceFolderController public readonly documentsManager: Documents public readonly contentProvider: ContentProvider public readonly autocmds: Autocmds public readonly watchers: Watchers public readonly keymaps: Keymaps public readonly files: Files public readonly fileSystemWatchers: FileSystemWatcherManager public readonly editors: Editors public readonly tabs: TabsModel public readonly isTrusted = true public statusLine = new StatusLine() private _onDidRuntimePathChange = new Emitter() public readonly onDidRuntimePathChange: Event = this._onDidRuntimePathChange.event private fuzzyExports: FuzzyWasi private strWidth: StrWidth private _env: Env constructor() { void initFuzzyWasm().then(api => { this.fuzzyExports = api }, onUnexpectedError) void StrWidth.create().then(strWidth => { this.strWidth = strWidth }, onUnexpectedError) events.on('VimResized', (columns, lines) => { Object.assign(toObject(this.env), { columns, lines }) }) Object.defineProperty(this.statusLine, 'nvim', { get: () => this.nvim }) this.configurations = new Configurations(userConfigFile, new ConfigurationShape(this)) this.workspaceFolderControl = new WorkspaceFolderController(this.configurations) let documents = this.documentsManager = new Documents(this.configurations, this.workspaceFolderControl) this.contentProvider = new ContentProvider(documents) this.watchers = new Watchers() this.autocmds = new Autocmds() this.keymaps = new Keymaps() this.files = new Files(documents, this.configurations, this.workspaceFolderControl, this.keymaps) this.editors = new Editors(documents) this.tabs = new TabsModel(this.editors) this.onDidChangeWorkspaceFolders = this.workspaceFolderControl.onDidChangeWorkspaceFolders this.onDidChangeConfiguration = this.configurations.onDidChange this.onDidOpenTextDocument = documents.onDidOpenTextDocument this.onDidChangeTextDocument = documents.onDidChangeDocument this.onDidCloseTextDocument = documents.onDidCloseDocument this.onDidSaveTextDocument = documents.onDidSaveTextDocument this.onWillSaveTextDocument = documents.onWillSaveTextDocument this.onDidCreateFiles = this.files.onDidCreateFiles this.onDidRenameFiles = this.files.onDidRenameFiles this.onDidDeleteFiles = this.files.onDidDeleteFiles this.onWillCreateFiles = this.files.onWillCreateFiles this.onWillRenameFiles = this.files.onWillRenameFiles this.onWillDeleteFiles = this.files.onWillDeleteFiles // use global value only const config = this.getWatchConfig() this.fileSystemWatchers = new FileSystemWatcherManager(this.workspaceFolderControl, config) } public get initialConfiguration(): WorkspaceConfiguration { return this.configurations.initialConfiguration } public getWatchConfig(): FileWatchConfig { let { initialConfiguration } = this let watchConfig = defaultValue>(initialConfiguration.get('fileSystemWatch'), {}) let watchmanPath = watchConfig.watchmanPath if (!watchmanPath) watchmanPath = initialConfiguration.inspect('coc.preferences.watchmanPath').globalValue if (typeof watchmanPath === 'string') watchmanPath = this.expand(watchmanPath) let ignoredFolders = defaultValue(watchConfig.ignoredFolders, ["${tmpdir}", "/private/tmp", "/"]) let enable = getConditionValue(watchConfig.enable == null ? true : !!watchConfig.enable, false) return { watchmanPath, enable, ignoredFolders: ignoredFolders.map(p => this.expand(p)) } } public async init(window: any): Promise { let { nvim } = this for (let method of methods) { Object.defineProperty(this, method, { get: () => { return (...args: any[]) => { let stack = '\n' + Error().stack.split('\n').slice(2, 4).join('\n') logger.warn(`workspace.${method} is deprecated, please use window.${method} instead.`, stack) return window[method].apply(window, args) } } }) } for (let name of ['onDidOpenTerminal', 'onDidCloseTerminal']) { Object.defineProperty(this, name, { get: () => { let stack = '\n' + Error().stack.split('\n').slice(2, 4).join('\n') logger.warn(`workspace.${name} is deprecated, please use window.${name} instead.`, stack) return window[name] } }) } let env = this._env = await nvim.call('coc#util#vim_info') as Env this.checkVersion(APIVERSION) this.configurations.updateMemoryConfig(this._env.config) this.workspaceFolderControl.setWorkspaceFolders(this._env.workspaceFolders) this.workspaceFolderControl.onDidChangeWorkspaceFolders(() => { nvim.setVar('WorkspaceFolders', this.folderPaths, true) }) this.files.attach(nvim, env, window) this.contentProvider.attach(nvim) this.registerTextDocumentContentProvider('output', channels.getProvider(nvim)) this.keymaps.attach(nvim) this.autocmds.attach(nvim) this.watchers.attach(nvim, env) this.watchers.watchOption('runtimepath', async (oldValue: string, newValue: string) => { let oldList: string[] = oldValue.split(',') let newList: string[] = newValue.split(',') let paths = newList.filter(x => !oldList.includes(x)) if (paths.length > 0) { let filepaths: string[] = [] await Promise.allSettled(paths.map(filepath => { return new Promise((resolve, reject) => { let converted = this.fixWin32unixFilepath(filepath) getFileType(converted).then(t => { if (t == FileType.Directory) { filepaths.push(converted) } resolve(undefined) }, reject) }) })) if (filepaths.length > 0) { this._onDidRuntimePathChange.fire(filepaths) this.env.runtimepath = [...oldList, ...filepaths].join(',') } } }) await this.documentsManager.attach(this.nvim, this._env) await this.editors.attach(nvim) this.tabs.attach() let channel = channels.create('watchman', nvim) this.fileSystemWatchers.attach(channel) if (this.strWidth) this.strWidth.setAmbw(!env.ambiguousIsNarrow) } public checkVersion(version: number) { if (this._env.apiversion != version) { this.nvim.echoError(`API version ${this._env.apiversion} is not ${APIVERSION}, building coc.nvim by 'npm ci'.`) this.nvim.call('coc#ui#fix', [], true) } } public getDisplayWidth(text: string, cache = false): number { return this.strWidth.getWidth(text, cache) } public get version(): string { return VERSION } public get cwd(): string { return this.documentsManager.cwd } public get env(): Env { return this._env } public get root(): string { return this.documentsManager.root || this.cwd } public get rootPath(): string { return this.root } public get bufnr(): number { return this.documentsManager.bufnr } /** * @deprecated */ public get insertMode(): boolean { return events.insertMode } /** * @deprecated always true */ public get floatSupported(): boolean { return true } /** * @deprecated */ public get uri(): string { return this.documentsManager.uri } /** * @deprecated */ public get workspaceFolder(): WorkspaceFolder { return this.workspaceFolders[0] } public get textDocuments(): TextDocument[] { return this.documentsManager.textDocuments } public get documents(): Document[] { return this.documentsManager.documents } public get document(): Promise { return this.documentsManager.document } public get workspaceFolders(): ReadonlyArray { return this.workspaceFolderControl.workspaceFolders } public fixWin32unixFilepath(filepath: string): string { return this.documentsManager.fixUnixPrefix(filepath) } public checkPatterns(patterns: string[], folders?: WorkspaceFolder[]): Promise { return this.workspaceFolderControl.checkPatterns(folders ?? this.workspaceFolderControl.workspaceFolders, patterns) } public get folderPaths(): string[] { return this.workspaceFolders.map(f => URI.parse(f.uri).fsPath) } public get channelNames(): string[] { return channels.names } public get pluginRoot(): string { return pluginRoot } public get isVim(): boolean { return this._env.isVim } public get isNvim(): boolean { return !this._env.isVim } /** * Kept for backward compatible */ public get completeOpt(): string { return '' } public get filetypes(): Set { return this.documentsManager.filetypes } public get languageIds(): Set { return this.documentsManager.languageIds } /** * @deprecated Use nvim.createNamespace() instead. */ public createNameSpace(name: string): number { return createNameSpace(name) } public has(feature: string): boolean { return has(this.env, feature) } /** * Register autocmd on vim. */ public registerAutocmd(autocmd: Autocmd, disposables?: Disposable[]): Disposable { let opts = Object.assign({}, autocmd) Error.captureStackTrace(opts) let disposable = this.autocmds.registerAutocmd(opts as AutocmdOptionWithStack) if (disposables) disposables.push(disposable) return disposable } /** * Watch for option change. */ public watchOption(key: string, callback: (oldValue: any, newValue: any) => Thenable | void, disposables?: Disposable[]): Disposable { return this.watchers.watchOption(key, callback, disposables) } /** * Watch global variable, works on neovim only. */ public watchGlobal(key: string, callback?: (oldValue: any, newValue: any) => Thenable | void, disposables?: Disposable[]): Disposable { let cb = callback ?? function() {} return this.watchers.watchGlobal(key, cb, disposables) } /** * Check if selector match document. */ public match(selector: DocumentSelector | DocumentFilter | string, document: TextDocumentMatch): number { return score(selector, document.uri, document.languageId) } /** * Create a FileSystemWatcher instance, doesn't fail when watchman not found. */ public createFileSystemWatcher(globPattern: GlobPattern, ignoreCreate?: boolean, ignoreChange?: boolean, ignoreDelete?: boolean): FileSystemWatcher { return this.fileSystemWatchers.createFileSystemWatcher(globPattern, ignoreCreate, ignoreChange, ignoreDelete) } public createFuzzyMatch(): FuzzyMatch { return new FuzzyMatch(this.fuzzyExports) } public getWatchmanPath(): string | null { return getWatchmanPath(this.configurations) } /** * Get configuration by section and optional resource uri. */ public getConfiguration(section?: string, scope?: ConfigurationResourceScope): WorkspaceConfiguration { return this.configurations.getConfiguration(section, scope) } public resolveJSONSchema(uri: string): IJSONSchema | undefined { return this.configurations.getJSONSchema(uri) } /** * Get created document by uri or bufnr. */ public getDocument(uri: number | string): Document | null | undefined { return this.documentsManager.getDocument(uri) } public hasDocument(uri: string, version?: number): boolean { let doc = this.documentsManager.getDocument(uri) return doc && (version != null ? doc.version == version : true) } public getUri(bufnr: number, defaultValue = ''): string { let doc = this.documentsManager.getDocument(bufnr) return doc ? doc.uri : defaultValue } public isAttached(bufnr: number): boolean { let doc = this.documentsManager.getDocument(bufnr) return doc != null && doc.attached } /** * Get attached document by uri or bufnr. * Throw error when document doesn't exist or isn't attached. */ public getAttachedDocument(uri: number | string): Document { let doc = this.getDocument(uri) if (!doc) throw new Error(`Buffer ${uri} not exists.`) if (!doc.attached) throw new Error(`Buffer ${uri} not attached, ${doc.notAttachReason}`) return doc } /** * Convert location to quickfix item. */ public getQuickfixItem(loc: Location | LocationLink, text?: string, type = '', module?: string): Promise { return this.documentsManager.getQuickfixItem(loc, text, type, module) } /** * Create persistence Mru instance. */ public createMru(name: string): Mru { return new Mru(name) } public async getQuickfixList(locations: Location[]): Promise> { return this.documentsManager.getQuickfixList(locations) } /** * Populate locations to UI. */ public async showLocations(locations: LocationWithTarget[]): Promise { await this.documentsManager.showLocations(locations) } /** * Get content of line by uri and line. */ public getLine(uri: string, line: number): Promise { return this.documentsManager.getLine(uri, line) } /** * Get WorkspaceFolder of uri */ public getWorkspaceFolder(uri: string | URI): WorkspaceFolder | undefined { return this.workspaceFolderControl.getWorkspaceFolder(typeof uri === 'string' ? URI.parse(uri) : uri) } /** * Get content from buffer or file by uri. */ public readFile(uri: string): Promise { return this.documentsManager.readFile(uri) } public async getCurrentState(): Promise<{ document: LinesTextDocument, position: Position }> { let document = await this.document let position = await ui.getCursorPosition(this.nvim) return { document: document.textDocument, position } } public async getFormatOptions(uri?: string | number): Promise { return this.documentsManager.getFormatOptions(uri) } /** * Resolve module from yarn or npm. */ public resolveModule(name: string): Promise { return resolveModule(name) } /** * Run nodejs command */ public async runCommand(cmd: string, cwd?: string, timeout?: number): Promise { return runCommand(cmd, { cwd: cwd ?? this.cwd }, timeout) } /** * Expand filepath with `~` and/or environment placeholders */ public expand(filepath: string): string { return this.documentsManager.expand(filepath) } public async callAsync(method: string, args: any[]): Promise { return await callAsync(this.nvim, method, args) } public registerTextDocumentContentProvider(scheme: string, provider: TextDocumentContentProvider): Disposable { return this.contentProvider.registerTextDocumentContentProvider(scheme, provider) } public registerKeymap(modes: MapMode[], key: string, fn: KeymapCallback, opts: Partial = {}): Disposable { return this.keymaps.registerKeymap(modes, key, fn, opts) } public registerExprKeymap(mode: 'i' | 'n' | 'v' | 's' | 'x', key: string, fn: KeymapCallback, buffer = false, cancel = true): Disposable { return this.keymaps.registerExprKeymap(mode, key, fn, buffer, cancel) } public registerLocalKeymap(bufnr: number, mode: LocalMode, key: string, fn: KeymapCallback, notify: KeymapOption | boolean = false): Disposable { if (typeof arguments[0] === 'string') { bufnr = this.bufnr mode = arguments[0] as LocalMode key = arguments[1] fn = arguments[2] notify = arguments[3] ?? false } return this.keymaps.registerLocalKeymap(bufnr, mode, key, fn, notify) } /** * Create Task instance that runs in vim. */ public createTask(id: string): Task { return new Task(this.nvim, id) } /** * Create DB instance at extension root. */ public createDatabase(name: string): DB { return new DB(path.join(dataHome, name + '.json')) } public registerBufferSync(create: (doc: Document) => T | undefined): BufferSync { return new BufferSync(create, this.documentsManager) } public async attach(): Promise { await this.documentsManager.attach(this.nvim, this._env) } public jumpTo(uri: string | URI, position?: Position | null, openCommand?: string): Promise { return this.files.jumpTo(uri, position, openCommand) } /** * Findup for filename or filenames from current filepath or root. */ public findUp(filename: string | string[]): Promise { return findUp(this.nvim, this.cwd, filename) } /** * Apply WorkspaceEdit. */ public applyEdit(edit: WorkspaceEdit): Promise { return this.files.applyEdit(edit) } /** * Create a file in vim and disk */ public createFile(filepath: string, opts: CreateFileOptions = {}): Promise { return this.files.createFile(filepath, opts) } /** * Load uri as document. */ public loadFile(uri: string, cmd?: string): Promise { return this.files.loadResource(uri, cmd) } /** * Load the files that not loaded */ public async loadFiles(uris: string[]): Promise<(Document | undefined)[]> { return this.files.loadResources(uris) } /** * Rename file in vim and disk */ public async renameFile(oldPath: string, newPath: string, opts: RenameFileOptions = {}): Promise { await this.files.renameFile(oldPath, newPath, opts) } /** * Delete file from vim and disk. */ public async deleteFile(filepath: string, opts: DeleteFileOptions = {}): Promise { await this.files.deleteFile(filepath, opts) } /** * Open resource by uri */ public async openResource(uri: string): Promise { await this.files.openResource(uri) } public async computeWordRanges(uri: string | number, range: Range, token?: CancellationToken): Promise<{ [word: string]: Range[] } | null> { let doc = this.getDocument(uri) if (!doc) return null return await doc.chars.computeWordRanges(doc.textDocument.lines, range, token) } public openTextDocument(uri: URI | string): Promise { return this.files.openTextDocument(uri) } public getRelativePath(pathOrUri: string | URI, includeWorkspace?: boolean): string { return this.workspaceFolderControl.getRelativePath(pathOrUri, includeWorkspace) } public asRelativePath(pathOrUri: string | URI, includeWorkspace?: boolean): string { return this.getRelativePath(pathOrUri, includeWorkspace) } public async findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Promise { return this.files.findFiles(include, exclude, maxResults, token) } public detach(): void { this.documentsManager.detach() } public reset(): void { this.statusLine.reset() this.configurations.reset() this.workspaceFolderControl.reset() this.documentsManager.reset() } public dispose(): void { channels.dispose() this.autocmds.dispose() this.statusLine.dispose() this.watchers.dispose() this.contentProvider.dispose() this.documentsManager.dispose() this.configurations.dispose() } } export default new Workspace() ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "lib", "strict": true, "noEmit": true, "sourceMap": true, "importHelpers": true, "allowUnreachableCode": false, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "noImplicitThis": true, "noUnusedLocals": false, "noUnusedParameters": false, "strictPropertyInitialization": false, "target": "es2020", "module": "es2020", "moduleResolution": "bundler", "lib": ["es2017", "es2018", "es2019", "ES2020.Promise", "Es2021"], "declaration": false, "resolveJsonModule": true, "esModuleInterop": true, "strictNullChecks": false, "strictFunctionTypes": true, "plugins": [] }, "include": ["src"], "exclude": ["typings", "build", "node_modules", "**/node_modules/*"] } ================================================ FILE: typings/LICENSE ================================================ Copyright 2020 chemzqm@gmail.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: typings/Readme.md ================================================ This package contains typings of coc.nvim only. Files were exported from https://github.com/neoclide/coc.nvim/blob/master/typings ## Installation npm install --save-dev coc.nvim ## Support coc.nvim If you like my work, consider supporting me on Patreon or PayPal: Patreon donate button PayPal donate button ## Credits [Visual Studio Code](https://github.com/microsoft/vscode), and [Microsoft](https://github.com/microsoft) ================================================ FILE: typings/index.d.ts ================================================ /****************************************************************** MIT License http://www.opensource.org/licenses/mit-license.php Author Qiming Zhao (https://github.com/chemzqm) *******************************************************************/ /// declare module 'coc.nvim' { import cp from 'child_process' import { URL } from 'url' // language server types {{ /** * A tagging type for string properties that are actually document URIs. */ export type DocumentUri = string export namespace DocumentUri { function is(value: any): value is DocumentUri } /** * Defines an integer in the range of -2^31 to 2^31 - 1. */ export type integer = number export namespace integer { const MIN_VALUE = -2147483648 const MAX_VALUE = 2147483647 function is(value: any): value is integer } /** * Defines an unsigned integer in the range of 0 to 2^31 - 1. */ export type uinteger = number export namespace uinteger { const MIN_VALUE = 0 const MAX_VALUE = 2147483647 function is(value: any): value is uinteger } /** * Defines a decimal number. Since decimal numbers are very * rare in the language server specification we denote the * exact range with every decimal using the mathematics * interval notations (e.g. [0, 1] denotes all decimals d with * 0 <= d <= 1. */ export type decimal = number /** * The LSP any type. * * In the current implementation we map LSPAny to any. This is due to the fact * that the TypeScript compilers can't infer string access signatures for * interface correctly (it can though for types). See the following issue for * details: https://github.com/microsoft/TypeScript/issues/15300. * * When the issue is addressed LSPAny can be defined as follows: * * ```ts * export type LSPAny = LSPObject | LSPArray | string | integer | uinteger | decimal | boolean | null | undefined; * export type LSPObject = { [key: string]: LSPAny }; * export type LSPArray = LSPAny[]; * ``` * * Please note that strictly speaking a property with the value `undefined` * can't be converted into JSON preserving the property name. However for * convenience it is allowed and assumed that all these properties are * optional as well. * * @since 3.17.0 */ export type LSPAny = any export type LSPObject = object export type LSPArray = any[] /** * Position in a text document expressed as zero-based line and character * offset. Prior to 3.17 the offsets were always based on a UTF-16 string * representation. So a string of the form `a𐐀b` the character offset of the * character `a` is 0, the character offset of `𐐀` is 1 and the character * offset of b is 3 since `𐐀` is represented using two code units in UTF-16. * Since 3.17 clients and servers can agree on a different string encoding * representation (e.g. UTF-8). The client announces it's supported encoding * via the client capability [`general.positionEncodings`](#clientCapabilities). * The value is an array of position encodings the client supports, with * decreasing preference (e.g. the encoding at index `0` is the most preferred * one). To stay backwards compatible the only mandatory encoding is UTF-16 * represented via the string `utf-16`. The server can pick one of the * encodings offered by the client and signals that encoding back to the * client via the initialize result's property * [`capabilities.positionEncoding`](#serverCapabilities). If the string value * `utf-16` is missing from the client's capability `general.positionEncodings` * servers can safely assume that the client supports UTF-16. If the server * omits the position encoding in its initialize result the encoding defaults * to the string value `utf-16`. Implementation considerations: since the * conversion from one encoding into another requires the content of the * file / line the conversion is best done where the file is read which is * usually on the server side. * * Positions are line end character agnostic. So you can not specify a position * that denotes `\r|\n` or `\n|` where `|` represents the character offset. * * @since 3.17.0 - support for negotiated position encoding. */ export interface Position { /** * Line position in a document (zero-based). * * If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. * If a line number is negative, it defaults to 0. */ line: uinteger /** * Character offset on a line in a document (zero-based). * * The meaning of this offset is determined by the negotiated * `PositionEncodingKind`. * * If the character value is greater than the line length it defaults back to the * line length. */ character: uinteger } /** * The Position namespace provides helper functions to work with * [Position](#Position) literals. */ export namespace Position { /** * Creates a new Position literal from the given line and character. * @param line The position's line. * @param character The position's character. */ function create(line: uinteger, character: uinteger): Position /** * Checks whether the given literal conforms to the [Position](#Position) interface. */ function is(value: any): value is Position } /** * A range in a text document expressed as (zero-based) start and end positions. * * If you want to specify a range that contains a line including the line ending * character(s) then use an end position denoting the start of the next line. * For example: * ```ts * { * start: { line: 5, character: 23 } * end : { line 6, character : 0 } * } * ``` */ export interface Range { /** * The range's start position. */ start: Position /** * The range's end position. */ end: Position } /** * The Range namespace provides helper functions to work with * [Range](#Range) literals. */ export namespace Range { /** * Create a new Range literal. * @param start The range's start position. * @param end The range's end position. */ function create(start: Position, end: Position): Range /** * Create a new Range literal. * @param startLine The start line number. * @param startCharacter The start character. * @param endLine The end line number. * @param endCharacter The end character. */ function create(startLine: uinteger, startCharacter: uinteger, endLine: uinteger, endCharacter: uinteger): Range /** * Checks whether the given literal conforms to the [Range](#Range) interface. */ function is(value: any): value is Range } /** * Represents a location inside a resource, such as a line * inside a text file. */ export interface Location { uri: DocumentUri range: Range } /** * The Location namespace provides helper functions to work with * [Location](#Location) literals. */ export namespace Location { /** * Creates a Location literal. * @param uri The location's uri. * @param range The location's range. */ function create(uri: DocumentUri, range: Range): Location /** * Checks whether the given literal conforms to the [Location](#Location) interface. */ function is(value: any): value is Location } /** * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), * including an origin range. */ export interface LocationLink { /** * Span of the origin of this link. * * Used as the underlined span for mouse interaction. Defaults to the word range at * the definition position. */ originSelectionRange?: Range /** * The target resource identifier of this link. */ targetUri: DocumentUri /** * The full target range of this link. If the target for example is a symbol then target range is the * range enclosing this symbol not including leading/trailing whitespace but everything else * like comments. This information is typically used to highlight the range in the editor. */ targetRange: Range /** * The range that should be selected and revealed when this link is being followed, e.g the name of a function. * Must be contained by the `targetRange`. See also `DocumentSymbol#range` */ targetSelectionRange: Range } /** * The LocationLink namespace provides helper functions to work with * [LocationLink](#LocationLink) literals. */ export namespace LocationLink { /** * Creates a LocationLink literal. * @param targetUri The definition's uri. * @param targetRange The full range of the definition. * @param targetSelectionRange The span of the symbol definition at the target. * @param originSelectionRange The span of the symbol being defined in the originating source file. */ function create(targetUri: DocumentUri, targetRange: Range, targetSelectionRange: Range, originSelectionRange?: Range): LocationLink /** * Checks whether the given literal conforms to the [LocationLink](#LocationLink) interface. */ function is(value: any): value is LocationLink } /** * Represents a color in RGBA space. */ export interface Color { /** * The red component of this color in the range [0-1]. */ readonly red: decimal /** * The green component of this color in the range [0-1]. */ readonly green: decimal /** * The blue component of this color in the range [0-1]. */ readonly blue: decimal /** * The alpha component of this color in the range [0-1]. */ readonly alpha: decimal } /** * The Color namespace provides helper functions to work with * [Color](#Color) literals. */ export namespace Color { /** * Creates a new Color literal. */ function create(red: decimal, green: decimal, blue: decimal, alpha: decimal): Color /** * Checks whether the given literal conforms to the [Color](#Color) interface. */ function is(value: any): value is Color } /** * Represents a color range from a document. */ export interface ColorInformation { /** * The range in the document where this color appears. */ range: Range /** * The actual color value for this color range. */ color: Color } /** * The ColorInformation namespace provides helper functions to work with * [ColorInformation](#ColorInformation) literals. */ export namespace ColorInformation { /** * Creates a new ColorInformation literal. */ function create(range: Range, color: Color): ColorInformation /** * Checks whether the given literal conforms to the [ColorInformation](#ColorInformation) interface. */ function is(value: any): value is ColorInformation } export interface ColorPresentation { /** * The label of this color presentation. It will be shown on the color * picker header. By default this is also the text that is inserted when selecting * this color presentation. */ label: string /** * An [edit](#TextEdit) which is applied to a document when selecting * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) * is used. */ textEdit?: TextEdit /** * An optional array of additional [text edits](#TextEdit) that are applied when * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. */ additionalTextEdits?: TextEdit[] } /** * The Color namespace provides helper functions to work with * [ColorPresentation](#ColorPresentation) literals. */ export namespace ColorPresentation { /** * Creates a new ColorInformation literal. */ function create(label: string, textEdit?: TextEdit, additionalTextEdits?: TextEdit[]): ColorPresentation /** * Checks whether the given literal conforms to the [ColorInformation](#ColorInformation) interface. */ function is(value: any): value is ColorPresentation } /** * A set of predefined range kinds. */ export namespace FoldingRangeKind { /** * Folding range for a comment */ const Comment = "comment" /** * Folding range for an import or include */ const Imports = "imports" /** * Folding range for a region (e.g. `#region`) */ const Region = "region" } /** * A predefined folding range kind. * * The type is a string since the value set is extensible */ export type FoldingRangeKind = string /** * Represents a folding range. To be valid, start and end line must be bigger than zero and smaller * than the number of lines in the document. Clients are free to ignore invalid ranges. */ export interface FoldingRange { /** * The zero-based start line of the range to fold. The folded area starts after the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. */ startLine: uinteger /** * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. */ startCharacter?: uinteger /** * The zero-based end line of the range to fold. The folded area ends with the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. */ endLine: uinteger /** * The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. */ endCharacter?: uinteger /** * Describes the kind of the folding range such as `comment' or 'region'. The kind * is used to categorize folding ranges and used by commands like 'Fold all comments'. * See [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. */ kind?: FoldingRangeKind /** * The text that the client should show when the specified range is * collapsed. If not defined or not supported by the client, a default * will be chosen by the client. * * @since 3.17.0 */ collapsedText?: string } /** * The folding range namespace provides helper functions to work with * [FoldingRange](#FoldingRange) literals. */ export namespace FoldingRange { /** * Creates a new FoldingRange literal. */ function create(startLine: uinteger, endLine: uinteger, startCharacter?: uinteger, endCharacter?: uinteger, kind?: FoldingRangeKind, collapsedText?: string): FoldingRange /** * Checks whether the given literal conforms to the [FoldingRange](#FoldingRange) interface. */ function is(value: any): value is FoldingRange } /** * Represents a related message and source code location for a diagnostic. This should be * used to point to code locations that cause or related to a diagnostics, e.g when duplicating * a symbol in a scope. */ export interface DiagnosticRelatedInformation { /** * The location of this related diagnostic information. */ location: Location /** * The message of this related diagnostic information. */ message: string } /** * The DiagnosticRelatedInformation namespace provides helper functions to work with * [DiagnosticRelatedInformation](#DiagnosticRelatedInformation) literals. */ export namespace DiagnosticRelatedInformation { /** * Creates a new DiagnosticRelatedInformation literal. */ function create(location: Location, message: string): DiagnosticRelatedInformation /** * Checks whether the given literal conforms to the [DiagnosticRelatedInformation](#DiagnosticRelatedInformation) interface. */ function is(value: any): value is DiagnosticRelatedInformation } /** * The diagnostic's severity. */ export namespace DiagnosticSeverity { /** * Reports an error. */ const Error: 1 /** * Reports a warning. */ const Warning: 2 /** * Reports an information. */ const Information: 3 /** * Reports a hint. */ const Hint: 4 } export type DiagnosticSeverity = 1 | 2 | 3 | 4 /** * The diagnostic tags. * * @since 3.15.0 */ export namespace DiagnosticTag { /** * Unused or unnecessary code. * * Clients are allowed to render diagnostics with this tag faded out instead of having * an error squiggle. */ const Unnecessary: 1 /** * Deprecated or obsolete code. * * Clients are allowed to rendered diagnostics with this tag strike through. */ const Deprecated: 2 } export type DiagnosticTag = 1 | 2 /** * Structure to capture a description for an error code. * * @since 3.16.0 */ export interface CodeDescription { /** * An URI to open with more information about the diagnostic error. */ href: string } /** * The CodeDescription namespace provides functions to deal with descriptions for diagnostic codes. * * @since 3.16.0 */ export namespace CodeDescription { function is(value: any): value is CodeDescription } /** * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects * are only valid in the scope of a resource. */ export interface Diagnostic { /** * The range at which the message applies */ range: Range /** * The diagnostic's severity. Can be omitted. If omitted it is up to the * client to interpret diagnostics as error, warning, info or hint. */ severity?: DiagnosticSeverity /** * The diagnostic's code, which usually appear in the user interface. */ code?: integer | string /** * An optional property to describe the error code. * Requires the code field (above) to be present/not null. * * @since 3.16.0 */ codeDescription?: CodeDescription /** * A human-readable string describing the source of this * diagnostic, e.g. 'typescript' or 'super lint'. It usually * appears in the user interface. */ source?: string /** * The diagnostic's message. It usually appears in the user interface */ message: string /** * Additional metadata about the diagnostic. * * @since 3.15.0 */ tags?: DiagnosticTag[] /** * An array of related diagnostic information, e.g. when symbol-names within * a scope collide all definitions can be marked via this property. */ relatedInformation?: DiagnosticRelatedInformation[] /** * A data entry field that is preserved between a `textDocument/publishDiagnostics` * notification and `textDocument/codeAction` request. * * @since 3.16.0 */ data?: LSPAny } /** * The Diagnostic namespace provides helper functions to work with * [Diagnostic](#Diagnostic) literals. */ export namespace Diagnostic { /** * Creates a new Diagnostic literal. */ function create(range: Range, message: string, severity?: DiagnosticSeverity, code?: integer | string, source?: string, relatedInformation?: DiagnosticRelatedInformation[]): Diagnostic /** * Checks whether the given literal conforms to the [Diagnostic](#Diagnostic) interface. */ function is(value: any): value is Diagnostic } /** * Represents a reference to a command. Provides a title which * will be used to represent a command in the UI and, optionally, * an array of arguments which will be passed to the command handler * function when invoked. */ export interface Command { /** * Title of the command, like `save`. */ title: string /** * The identifier of the actual command handler. */ command: string /** * Arguments that the command handler should be * invoked with. */ arguments?: LSPAny[] } /** * The Command namespace provides helper functions to work with * [Command](#Command) literals. */ export namespace Command { /** * Creates a new Command literal. */ function create(title: string, command: string, ...args: any[]): Command /** * Checks whether the given literal conforms to the [Command](#Command) interface. */ function is(value: any): value is Command } /** * A text edit applicable to a text document. */ export interface TextEdit { /** * The range of the text document to be manipulated. To insert * text into a document create a range where start === end. */ range: Range /** * The string to be inserted. For delete operations use an * empty string. */ newText: string } /** * The TextEdit namespace provides helper function to create replace, * insert and delete edits more easily. */ export namespace TextEdit { /** * Creates a replace text edit. * @param range The range of text to be replaced. * @param newText The new text. */ function replace(range: Range, newText: string): TextEdit /** * Creates an insert text edit. * @param position The position to insert the text at. * @param newText The text to be inserted. */ function insert(position: Position, newText: string): TextEdit /** * Creates a delete text edit. * @param range The range of text to be deleted. */ function del(range: Range): TextEdit function is(value: any): value is TextEdit } /** * Additional information that describes document changes. * * @since 3.16.0 */ export interface ChangeAnnotation { /** * A human-readable string describing the actual change. The string * is rendered prominent in the user interface. */ label: string /** * A flag which indicates that user confirmation is needed * before applying the change. */ needsConfirmation?: boolean /** * A human-readable string which is rendered less prominent in * the user interface. */ description?: string } export namespace ChangeAnnotation { function create(label: string, needsConfirmation?: boolean, description?: string): ChangeAnnotation function is(value: any): value is ChangeAnnotation } export namespace ChangeAnnotationIdentifier { function is(value: any): value is ChangeAnnotationIdentifier } /** * An identifier to refer to a change annotation stored with a workspace edit. */ export type ChangeAnnotationIdentifier = string /** * A special text edit with an additional change annotation. * * @since 3.16.0. */ export interface AnnotatedTextEdit extends TextEdit { /** * The actual identifier of the change annotation */ annotationId: ChangeAnnotationIdentifier } export namespace AnnotatedTextEdit { /** * Creates an annotated replace text edit. * * @param range The range of text to be replaced. * @param newText The new text. * @param annotation The annotation. */ function replace(range: Range, newText: string, annotation: ChangeAnnotationIdentifier): AnnotatedTextEdit /** * Creates an annotated insert text edit. * * @param position The position to insert the text at. * @param newText The text to be inserted. * @param annotation The annotation. */ function insert(position: Position, newText: string, annotation: ChangeAnnotationIdentifier): AnnotatedTextEdit /** * Creates an annotated delete text edit. * * @param range The range of text to be deleted. * @param annotation The annotation. */ function del(range: Range, annotation: ChangeAnnotationIdentifier): AnnotatedTextEdit function is(value: any): value is AnnotatedTextEdit } /** * Describes textual changes on a text document. A TextDocumentEdit describes all changes * on a document version Si and after they are applied move the document to version Si+1. * So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any * kind of ordering. However the edits must be non overlapping. */ export interface TextDocumentEdit { /** * The text document to change. */ textDocument: OptionalVersionedTextDocumentIdentifier /** * The edits to be applied. * * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a * client capability. * * @since 3.18.0 - support for SnippetTextEdit. This is guarded using a * client capability. */ edits: (TextEdit | AnnotatedTextEdit | SnippetTextEdit)[] } /** * The TextDocumentEdit namespace provides helper function to create * an edit that manipulates a text document. */ export namespace TextDocumentEdit { /** * Creates a new `TextDocumentEdit` */ function create(textDocument: OptionalVersionedTextDocumentIdentifier, edits: (TextEdit | AnnotatedTextEdit)[]): TextDocumentEdit function is(value: any): value is TextDocumentEdit } /** * A generic resource operation. */ interface ResourceOperation { /** * The resource operation kind. */ kind: string /** * An optional annotation identifier describing the operation. * * @since 3.16.0 */ annotationId?: ChangeAnnotationIdentifier } /** * Options to create a file. */ export interface CreateFileOptions { /** * Overwrite existing file. Overwrite wins over `ignoreIfExists` */ overwrite?: boolean /** * Ignore if exists. */ ignoreIfExists?: boolean } /** * Create file operation. */ export interface CreateFile extends ResourceOperation { /** * A create */ kind: 'create' /** * The resource to create. */ uri: DocumentUri /** * Additional options */ options?: CreateFileOptions } export namespace CreateFile { function create(uri: DocumentUri, options?: CreateFileOptions, annotation?: ChangeAnnotationIdentifier): CreateFile function is(value: any): value is CreateFile } /** * Rename file options */ export interface RenameFileOptions { /** * Overwrite target if existing. Overwrite wins over `ignoreIfExists` */ overwrite?: boolean /** * Ignores if target exists. */ ignoreIfExists?: boolean } /** * Rename file operation */ export interface RenameFile extends ResourceOperation { /** * A rename */ kind: 'rename' /** * The old (existing) location. */ oldUri: DocumentUri /** * The new location. */ newUri: DocumentUri /** * Rename options. */ options?: RenameFileOptions } export namespace RenameFile { function create(oldUri: DocumentUri, newUri: DocumentUri, options?: RenameFileOptions, annotation?: ChangeAnnotationIdentifier): RenameFile function is(value: any): value is RenameFile } /** * Delete file options */ export interface DeleteFileOptions { /** * Delete the content recursively if a folder is denoted. */ recursive?: boolean /** * Ignore the operation if the file doesn't exist. */ ignoreIfNotExists?: boolean } /** * Delete file operation */ export interface DeleteFile extends ResourceOperation { /** * A delete */ kind: 'delete' /** * The file to delete. */ uri: DocumentUri /** * Delete options. */ options?: DeleteFileOptions } export namespace DeleteFile { function create(uri: DocumentUri, options?: DeleteFileOptions, annotation?: ChangeAnnotationIdentifier): DeleteFile function is(value: any): value is DeleteFile } /** * A workspace edit represents changes to many resources managed in the workspace. The edit * should either provide `changes` or `documentChanges`. If documentChanges are present * they are preferred over `changes` if the client can handle versioned document edits. * * Since version 3.13.0 a workspace edit can contain resource operations as well. If resource * operations are present clients need to execute the operations in the order in which they * are provided. So a workspace edit for example can consist of the following two changes: * (1) a create file a.txt and (2) a text document edit which insert text into file a.txt. * * An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will * cause failure of the operation. How the client recovers from the failure is described by * the client capability: `workspace.workspaceEdit.failureHandling` */ export interface WorkspaceEdit { /** * Holds changes to existing resources. */ changes?: { [uri: DocumentUri]: TextEdit[] } /** * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes * are either an array of `TextDocumentEdit`s to express changes to n different text documents * where each text document edit addresses a specific version of a text document. Or it can contain * above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. * * Whether a client supports versioned document edits is expressed via * `workspace.workspaceEdit.documentChanges` client capability. * * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then * only plain `TextEdit`s using the `changes` property are supported. */ documentChanges?: (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] /** * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and * delete file / folder operations. * * Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. * * @since 3.16.0 */ changeAnnotations?: { [id: ChangeAnnotationIdentifier]: ChangeAnnotation } } export namespace WorkspaceEdit { function is(value: any): value is WorkspaceEdit } /** * A change to capture text edits for existing resources. */ export interface TextEditChange { /** * Gets all text edits for this change. * * @return An array of text edits. * * @since 3.16.0 - support for annotated text edits. This is usually * guarded using a client capability. */ all(): (TextEdit | AnnotatedTextEdit)[] /** * Clears the edits for this change. */ clear(): void /** * Adds a text edit. * * @param edit the text edit to add. * * @since 3.16.0 - support for annotated text edits. This is usually * guarded using a client capability. */ add(edit: TextEdit | AnnotatedTextEdit): void /** * Insert the given text at the given position. * * @param position A position. * @param newText A string. * @param annotation An optional annotation. */ insert(position: Position, newText: string): void insert(position: Position, newText: string, annotation: ChangeAnnotation | ChangeAnnotationIdentifier): ChangeAnnotationIdentifier /** * Replace the given range with given text for the given resource. * * @param range A range. * @param newText A string. * @param annotation An optional annotation. */ replace(range: Range, newText: string): void replace(range: Range, newText: string, annotation?: ChangeAnnotation | ChangeAnnotationIdentifier): ChangeAnnotationIdentifier /** * Delete the text at the given range. * * @param range A range. * @param annotation An optional annotation. */ delete(range: Range): void delete(range: Range, annotation?: ChangeAnnotation | ChangeAnnotationIdentifier): ChangeAnnotationIdentifier } /** * A workspace change helps constructing changes to a workspace. */ export class WorkspaceChange { private _workspaceEdit private _textEditChanges private _changeAnnotations constructor(workspaceEdit?: WorkspaceEdit) /** * Returns the underlying [WorkspaceEdit](#WorkspaceEdit) literal * use to be returned from a workspace edit operation like rename. */ get edit(): WorkspaceEdit /** * Returns the [TextEditChange](#TextEditChange) to manage text edits * for resources. */ getTextEditChange(textDocument: OptionalVersionedTextDocumentIdentifier): TextEditChange getTextEditChange(uri: DocumentUri): TextEditChange private initDocumentChanges private initChanges createFile(uri: DocumentUri, options?: CreateFileOptions): void createFile(uri: DocumentUri, annotation: ChangeAnnotation | ChangeAnnotationIdentifier, options?: CreateFileOptions): ChangeAnnotationIdentifier renameFile(oldUri: DocumentUri, newUri: DocumentUri, options?: RenameFileOptions): void renameFile(oldUri: DocumentUri, newUri: DocumentUri, annotation?: ChangeAnnotation | ChangeAnnotationIdentifier, options?: RenameFileOptions): ChangeAnnotationIdentifier deleteFile(uri: DocumentUri, options?: DeleteFileOptions): void deleteFile(uri: DocumentUri, annotation: ChangeAnnotation | ChangeAnnotationIdentifier, options?: DeleteFileOptions): ChangeAnnotationIdentifier } /** * A literal to identify a text document in the client. */ export interface TextDocumentIdentifier { /** * The text document's uri. */ uri: DocumentUri } /** * The TextDocumentIdentifier namespace provides helper functions to work with * [TextDocumentIdentifier](#TextDocumentIdentifier) literals. */ export namespace TextDocumentIdentifier { /** * Creates a new TextDocumentIdentifier literal. * @param uri The document's uri. */ function create(uri: DocumentUri): TextDocumentIdentifier /** * Checks whether the given literal conforms to the [TextDocumentIdentifier](#TextDocumentIdentifier) interface. */ function is(value: any): value is TextDocumentIdentifier } /** * A text document identifier to denote a specific version of a text document. */ export interface VersionedTextDocumentIdentifier extends TextDocumentIdentifier { /** * The version number of this document. */ version: integer } /** * The VersionedTextDocumentIdentifier namespace provides helper functions to work with * [VersionedTextDocumentIdentifier](#VersionedTextDocumentIdentifier) literals. */ export namespace VersionedTextDocumentIdentifier { /** * Creates a new VersionedTextDocumentIdentifier literal. * @param uri The document's uri. * @param version The document's version. */ function create(uri: DocumentUri, version: integer): VersionedTextDocumentIdentifier /** * Checks whether the given literal conforms to the [VersionedTextDocumentIdentifier](#VersionedTextDocumentIdentifier) interface. */ function is(value: any): value is VersionedTextDocumentIdentifier } /** * A text document identifier to optionally denote a specific version of a text document. */ export interface OptionalVersionedTextDocumentIdentifier extends TextDocumentIdentifier { /** * The version number of this document. If a versioned text document identifier * is sent from the server to the client and the file is not open in the editor * (the server has not received an open notification before) the server can send * `null` to indicate that the version is unknown and the content on disk is the * truth (as specified with document content ownership). */ version: integer | null } /** * The OptionalVersionedTextDocumentIdentifier namespace provides helper functions to work with * [OptionalVersionedTextDocumentIdentifier](#OptionalVersionedTextDocumentIdentifier) literals. */ export namespace OptionalVersionedTextDocumentIdentifier { /** * Creates a new OptionalVersionedTextDocumentIdentifier literal. * @param uri The document's uri. * @param version The document's version. */ function create(uri: DocumentUri, version: integer | null): OptionalVersionedTextDocumentIdentifier /** * Checks whether the given literal conforms to the [OptionalVersionedTextDocumentIdentifier](#OptionalVersionedTextDocumentIdentifier) interface. */ function is(value: any): value is OptionalVersionedTextDocumentIdentifier } /** * An item to transfer a text document from the client to the * server. */ export interface TextDocumentItem { /** * The text document's uri. */ uri: DocumentUri /** * The text document's language identifier. */ languageId: string /** * The version number of this document (it will increase after each * change, including undo/redo). */ version: integer /** * The content of the opened text document. */ text: string } /** * The TextDocumentItem namespace provides helper functions to work with * [TextDocumentItem](#TextDocumentItem) literals. */ export namespace TextDocumentItem { /** * Creates a new TextDocumentItem literal. * @param uri The document's uri. * @param languageId The document's language identifier. * @param version The document's version number. * @param text The document's text. */ function create(uri: DocumentUri, languageId: string, version: integer, text: string): TextDocumentItem /** * Checks whether the given literal conforms to the [TextDocumentItem](#TextDocumentItem) interface. */ function is(value: any): value is TextDocumentItem } /** * Describes the content type that a client supports in various * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. * * Please note that `MarkupKinds` must not start with a `$`. This kinds * are reserved for internal usage. */ export namespace MarkupKind { /** * Plain text is supported as a content format */ const PlainText: 'plaintext' /** * Markdown is supported as a content format */ const Markdown: 'markdown' /** * Checks whether the given value is a value of the [MarkupKind](#MarkupKind) type. */ function is(value: any): value is MarkupKind } export type MarkupKind = 'plaintext' | 'markdown' /** * A `MarkupContent` literal represents a string value which content is interpreted base on its * kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. * * If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. * See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting * * Here is an example how such a string can be constructed using JavaScript / TypeScript: * ```ts * let markdown: MarkdownContent = { * kind: MarkupKind.Markdown, * value: [ * '# Header', * 'Some text', * '```typescript', * 'someCode();', * '```' * ].join('\n') * }; * ``` * * *Please Note* that clients might sanitize the return markdown. A client could decide to * remove HTML from the markdown to avoid script execution. */ export interface MarkupContent { /** * The type of the Markup */ kind: MarkupKind /** * The content itself */ value: string } export namespace MarkupContent { /** * Checks whether the given value conforms to the [MarkupContent](#MarkupContent) interface. */ function is(value: any): value is MarkupContent } /** * The kind of a completion entry. */ export namespace CompletionItemKind { const Text: 1 const Method: 2 const Function: 3 const Constructor: 4 const Field: 5 const Variable: 6 const Class: 7 const Interface: 8 const Module: 9 const Property: 10 const Unit: 11 const Value: 12 const Enum: 13 const Keyword: 14 const Snippet: 15 const Color: 16 const File: 17 const Reference: 18 const Folder: 19 const EnumMember: 20 const Constant: 21 const Struct: 22 const Event: 23 const Operator: 24 const TypeParameter: 25 } export type CompletionItemKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 /** * Defines whether the insert text in a completion item should be interpreted as * plain text or a snippet. */ export namespace InsertTextFormat { /** * The primary text to be inserted is treated as a plain string. */ const PlainText: 1 /** * The primary text to be inserted is treated as a snippet. * * A snippet can define tab stops and placeholders with `$1`, `$2` * and `${3:foo}`. `$0` defines the final tab stop, it defaults to * the end of the snippet. Placeholders with equal identifiers are linked, * that is typing in one will update others too. * * See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax */ const Snippet: 2 } export type InsertTextFormat = 1 | 2 /** * Completion item tags are extra annotations that tweak the rendering of a completion * item. * * @since 3.15.0 */ export namespace CompletionItemTag { /** * Render a completion as obsolete, usually using a strike-out. */ const Deprecated = 1 } export type CompletionItemTag = 1 /** * A special text edit to provide an insert and a replace operation. * * @since 3.16.0 */ export interface InsertReplaceEdit { /** * The string to be inserted. */ newText: string /** * The range if the insert is requested */ insert: Range /** * The range if the replace is requested. */ replace: Range } /** * The InsertReplaceEdit namespace provides functions to deal with insert / replace edits. * * @since 3.16.0 */ export namespace InsertReplaceEdit { /** * Creates a new insert / replace edit */ function create(newText: string, insert: Range, replace: Range): InsertReplaceEdit /** * Checks whether the given literal conforms to the [InsertReplaceEdit](#InsertReplaceEdit) interface. */ function is(value: TextEdit | InsertReplaceEdit): value is InsertReplaceEdit } /** * How whitespace and indentation is handled during completion * item insertion. * * @since 3.16.0 */ export namespace InsertTextMode { /** * The insertion or replace strings is taken as it is. If the * value is multi line the lines below the cursor will be * inserted using the indentation defined in the string value. * The client will not apply any kind of adjustments to the * string. */ const asIs: 1 /** * The editor adjusts leading whitespace of new lines so that * they match the indentation up to the cursor of the line for * which the item is accepted. * * Consider a line like this: <2tabs><3tabs>foo. Accepting a * multi line completion item is indented using 2 tabs and all * following lines inserted will be indented using 2 tabs as well. */ const adjustIndentation: 2 } export type InsertTextMode = 1 | 2 /** * Additional details for a completion item label. * * @since 3.17.0 */ export interface CompletionItemLabelDetails { /** * An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, * without any spacing. Should be used for function signatures and type annotations. */ detail?: string /** * An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used * for fully qualified names and file paths. */ description?: string } export namespace CompletionItemLabelDetails { function is(value: any): value is CompletionItemLabelDetails } /** * A completion item represents a text snippet that is * proposed to complete text that is being typed. */ export interface CompletionItem { /** * The label of this completion item. * * The label property is also by default the text that * is inserted when selecting this completion. * * If label details are provided the label itself should * be an unqualified name of the completion item. */ label: string /** * Additional details for the label * * @since 3.17.0 */ labelDetails?: CompletionItemLabelDetails /** * The kind of this completion item. Based of the kind * an icon is chosen by the editor. */ kind?: CompletionItemKind /** * Tags for this completion item. * * @since 3.15.0 */ tags?: CompletionItemTag[] /** * A human-readable string with additional information * about this item, like type or symbol information. */ detail?: string /** * A human-readable string that represents a doc-comment. */ documentation?: string | MarkupContent /** * Indicates if this item is deprecated. * @deprecated Use `tags` instead. */ deprecated?: boolean /** * Select this item when showing. * * *Note* that only one completion item can be selected and that the * tool / client decides which item that is. The rule is that the *first* * item of those that match best is selected. */ preselect?: boolean /** * A string that should be used when comparing this item * with other items. When `falsy` the [label](#CompletionItem.label) * is used. */ sortText?: string /** * A string that should be used when filtering a set of * completion items. When `falsy` the [label](#CompletionItem.label) * is used. */ filterText?: string /** * A string that should be inserted into a document when selecting * this completion. When `falsy` the [label](#CompletionItem.label) * is used. * * The `insertText` is subject to interpretation by the client side. * Some tools might not take the string literally. For example * VS Code when code complete is requested in this example * `con` and a completion item with an `insertText` of * `console` is provided it will only insert `sole`. Therefore it is * recommended to use `textEdit` instead since it avoids additional client * side interpretation. */ insertText?: string /** * The format of the insert text. The format applies to both the * `insertText` property and the `newText` property of a provided * `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. * * Please note that the insertTextFormat doesn't apply to * `additionalTextEdits`. */ insertTextFormat?: InsertTextFormat /** * How whitespace and indentation is handled during completion * item insertion. If not provided the clients default value depends on * the `textDocument.completion.insertTextMode` client capability. * * @since 3.16.0 */ insertTextMode?: InsertTextMode /** * An [edit](#TextEdit) which is applied to a document when selecting * this completion. When an edit is provided the value of * [insertText](#CompletionItem.insertText) is ignored. * * Most editors support two different operations when accepting a completion * item. One is to insert a completion text and the other is to replace an * existing text with a completion text. Since this can usually not be * predetermined by a server it can report both ranges. Clients need to * signal support for `InsertReplaceEdits` via the * `textDocument.completion.insertReplaceSupport` client capability * property. * * *Note 1:* The text edit's range as well as both ranges from an insert * replace edit must be a [single line] and they must contain the position * at which completion has been requested. * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range * must be a prefix of the edit's replace range, that means it must be * contained and starting at the same position. * * @since 3.16.0 additional type `InsertReplaceEdit` */ textEdit?: TextEdit | InsertReplaceEdit /** * The edit text used if the completion item is part of a CompletionList and * CompletionList defines an item default for the text edit range. * * Clients will only honor this property if they opt into completion list * item defaults using the capability `completionList.itemDefaults`. * * If not provided and a list's default range is provided the label * property is used as a text. * * @since 3.17.0 */ textEditText?: string /** * An optional array of additional [text edits](#TextEdit) that are applied when * selecting this completion. Edits must not overlap (including the same insert position) * with the main [edit](#CompletionItem.textEdit) nor with themselves. * * Additional text edits should be used to change text unrelated to the current cursor position * (for example adding an import statement at the top of the file if the completion item will * insert an unqualified type). */ additionalTextEdits?: TextEdit[] /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous * characters will be ignored. */ commitCharacters?: string[] /** * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that * additional modifications to the current document should be described with the * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. */ command?: Command /** * A data entry field that is preserved on a completion item between a * [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest](#CompletionResolveRequest). */ data?: LSPAny } /** * The CompletionItem namespace provides functions to deal with * completion items. */ export namespace CompletionItem { /** * Create a completion item and seed it with a label. * @param label The completion item's label */ function create(label: string): CompletionItem } /** * Represents a collection of [completion items](#CompletionItem) to be presented * in the editor. */ export interface CompletionList { /** * This list it not complete. Further typing results in recomputing this list. * * Recomputed lists have all their items replaced (not appended) in the * incomplete completion sessions. */ isIncomplete: boolean /** * In many cases the items of an actual completion result share the same * value for properties like `commitCharacters` or the range of a text * edit. A completion list can therefore define item defaults which will * be used if a completion item itself doesn't specify the value. * * If a completion list specifies a default value and a completion item * also specifies a corresponding value the one from the item is used. * * Servers are only allowed to return default values if the client * signals support for this via the `completionList.itemDefaults` * capability. * * @since 3.17.0 */ itemDefaults?: { /** * A default commit character set. * * @since 3.17.0 */ commitCharacters?: string[] /** * A default edit range. * * @since 3.17.0 */ editRange?: Range | { insert: Range replace: Range } /** * A default insert text format. * * @since 3.17.0 */ insertTextFormat?: InsertTextFormat /** * A default insert text mode. * * @since 3.17.0 */ insertTextMode?: InsertTextMode /** * A default data value. * * @since 3.17.0 */ data?: LSPAny } /** * The completion items. */ items: CompletionItem[] } /** * The CompletionList namespace provides functions to deal with * completion lists. */ export namespace CompletionList { /** * Creates a new completion list. * * @param items The completion items. * @param isIncomplete The list is not complete. */ function create(items?: CompletionItem[], isIncomplete?: boolean): CompletionList } /** * MarkedString can be used to render human readable text. It is either a markdown string * or a code-block that provides a language and a code snippet. The language identifier * is semantically equal to the optional language identifier in fenced code blocks in GitHub * issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting * * The pair of a language and a value is an equivalent to markdown: * ```${language} * ${value} * ``` * * Note that markdown strings will be sanitized - that means html will be escaped. * @deprecated use MarkupContent instead. */ export type MarkedString = string | { language: string value: string } export namespace MarkedString { /** * Creates a marked string from plain text. * * @param plainText The plain text. */ function fromPlainText(plainText: string): string /** * Checks whether the given value conforms to the [MarkedString](#MarkedString) type. */ function is(value: any): value is MarkedString } /** * The result of a hover request. */ export interface Hover { /** * The hover's content */ contents: MarkupContent | MarkedString | MarkedString[] /** * An optional range inside the text document that is used to * visualize the hover, e.g. by changing the background color. */ range?: Range } export namespace Hover { /** * Checks whether the given value conforms to the [Hover](#Hover) interface. */ function is(value: any): value is Hover } /** * Represents a parameter of a callable-signature. A parameter can * have a label and a doc-comment. */ export interface ParameterInformation { /** * The label of this parameter information. * * Either a string or an inclusive start and exclusive end offsets within its containing * signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 * string representation as `Position` and `Range` does. * * *Note*: a label of type string should be a substring of its containing signature label. * Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. */ label: string | [uinteger, uinteger] /** * The human-readable doc-comment of this parameter. Will be shown * in the UI but can be omitted. */ documentation?: string | MarkupContent } /** * The ParameterInformation namespace provides helper functions to work with * [ParameterInformation](#ParameterInformation) literals. */ export namespace ParameterInformation { /** * Creates a new parameter information literal. * * @param label A label string. * @param documentation A doc string. */ function create(label: string | [uinteger, uinteger], documentation?: string): ParameterInformation } /** * Represents the signature of something callable. A signature * can have a label, like a function-name, a doc-comment, and * a set of parameters. */ export interface SignatureInformation { /** * The label of this signature. Will be shown in * the UI. */ label: string /** * The human-readable doc-comment of this signature. Will be shown * in the UI but can be omitted. */ documentation?: string | MarkupContent /** * The parameters of this signature. */ parameters?: ParameterInformation[] /** * The index of the active parameter. * * If provided, this is used in place of `SignatureHelp.activeParameter`. * * @since 3.16.0 */ activeParameter?: uinteger } /** * The SignatureInformation namespace provides helper functions to work with * [SignatureInformation](#SignatureInformation) literals. */ export namespace SignatureInformation { function create(label: string, documentation?: string, ...parameters: ParameterInformation[]): SignatureInformation } /** * Signature help represents the signature of something * callable. There can be multiple signature but only one * active and only one active parameter. */ export interface SignatureHelp { /** * One or more signatures. */ signatures: SignatureInformation[] /** * The active signature. If omitted or the value lies outside the * range of `signatures` the value defaults to zero or is ignored if * the `SignatureHelp` has no signatures. * * Whenever possible implementors should make an active decision about * the active signature and shouldn't rely on a default value. * * In future version of the protocol this property might become * mandatory to better express this. */ activeSignature?: uinteger /** * The active parameter of the active signature. If omitted or the value * lies outside the range of `signatures[activeSignature].parameters` * defaults to 0 if the active signature has parameters. If * the active signature has no parameters it is ignored. * In future version of the protocol this property might become * mandatory to better express the active parameter if the * active signature does have any. */ activeParameter?: uinteger } /** * The definition of a symbol represented as one or many [locations](#Location). * For most programming languages there is only one location at which a symbol is * defined. * * Servers should prefer returning `DefinitionLink` over `Definition` if supported * by the client. */ export type Definition = Location | Location[] /** * Information about where a symbol is defined. * * Provides additional metadata over normal [location](#Location) definitions, including the range of * the defining symbol */ export type DefinitionLink = LocationLink /** * The declaration of a symbol representation as one or many [locations](#Location). */ export type Declaration = Location | Location[] /** * Information about where a symbol is declared. * * Provides additional metadata over normal [location](#Location) declarations, including the range of * the declaring symbol. * * Servers should prefer returning `DeclarationLink` over `Declaration` if supported * by the client. */ export type DeclarationLink = LocationLink /** * Value-object that contains additional information when * requesting references. */ export interface ReferenceContext { /** * Include the declaration of the current symbol. */ includeDeclaration: boolean } /** * A document highlight kind. */ export namespace DocumentHighlightKind { /** * A textual occurrence. */ const Text: 1 /** * Read-access of a symbol, like reading a variable. */ const Read: 2 /** * Write-access of a symbol, like writing to a variable. */ const Write: 3 } export type DocumentHighlightKind = 1 | 2 | 3 /** * A document highlight is a range inside a text document which deserves * special attention. Usually a document highlight is visualized by changing * the background color of its range. */ export interface DocumentHighlight { /** * The range this highlight applies to. */ range: Range /** * The highlight kind, default is [text](#DocumentHighlightKind.Text). */ kind?: DocumentHighlightKind } /** * DocumentHighlight namespace to provide helper functions to work with * [DocumentHighlight](#DocumentHighlight) literals. */ export namespace DocumentHighlight { /** * Create a DocumentHighlight object. * @param range The range the highlight applies to. * @param kind The highlight kind */ function create(range: Range, kind?: DocumentHighlightKind): DocumentHighlight } /** * A symbol kind. */ export namespace SymbolKind { const File: 1 const Module: 2 const Namespace: 3 const Package: 4 const Class: 5 const Method: 6 const Property: 7 const Field: 8 const Constructor: 9 const Enum: 10 const Interface: 11 const Function: 12 const Variable: 13 const Constant: 14 const String: 15 const Number: 16 const Boolean: 17 const Array: 18 const Object: 19 const Key: 20 const Null: 21 const EnumMember: 22 const Struct: 23 const Event: 24 const Operator: 25 const TypeParameter: 26 } export type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 /** * Symbol tags are extra annotations that tweak the rendering of a symbol. * * @since 3.16 */ export namespace SymbolTag { /** * Render a symbol as obsolete, usually using a strike-out. */ const Deprecated: 1 } export type SymbolTag = 1 /** * A base for all symbol information. */ export interface BaseSymbolInformation { /** * The name of this symbol. */ name: string /** * The kind of this symbol. */ kind: SymbolKind /** * Tags for this symbol. * * @since 3.16.0 */ tags?: SymbolTag[] /** * The name of the symbol containing this symbol. This information is for * user interface purposes (e.g. to render a qualifier in the user interface * if necessary). It can't be used to re-infer a hierarchy for the document * symbols. */ containerName?: string } /** * Represents information about programming constructs like variables, classes, * interfaces etc. */ export interface SymbolInformation extends BaseSymbolInformation { /** * Indicates if this symbol is deprecated. * * @deprecated Use tags instead */ deprecated?: boolean /** * The location of this symbol. The location's range is used by a tool * to reveal the location in the editor. If the symbol is selected in the * tool the range's start information is used to position the cursor. So * the range usually spans more than the actual symbol's name and does * normally include things like visibility modifiers. * * The range doesn't have to denote a node range in the sense of an abstract * syntax tree. It can therefore not be used to re-construct a hierarchy of * the symbols. */ location: Location } export namespace SymbolInformation { /** * Creates a new symbol information literal. * * @param name The name of the symbol. * @param kind The kind of the symbol. * @param range The range of the location of the symbol. * @param uri The resource of the location of symbol. * @param containerName The name of the symbol containing the symbol. */ function create(name: string, kind: SymbolKind, range: Range, uri: DocumentUri, containerName?: string): SymbolInformation } /** * A special workspace symbol that supports locations without a range. * * See also SymbolInformation. * * @since 3.17.0 */ export interface WorkspaceSymbol extends BaseSymbolInformation { /** * The location of the symbol. Whether a server is allowed to * return a location without a range depends on the client * capability `workspace.symbol.resolveSupport`. * * See SymbolInformation#location for more details. */ location: Location | { uri: DocumentUri } /** * A data entry field that is preserved on a workspace symbol between a * workspace symbol request and a workspace symbol resolve request. */ data?: LSPAny } export namespace WorkspaceSymbol { /** * Create a new workspace symbol. * * @param name The name of the symbol. * @param kind The kind of the symbol. * @param uri The resource of the location of the symbol. * @param range An options range of the location. * @returns A WorkspaceSymbol. */ function create(name: string, kind: SymbolKind, uri: DocumentUri, range?: Range): WorkspaceSymbol } /** * Represents programming constructs like variables, classes, interfaces etc. * that appear in a document. Document symbols can be hierarchical and they * have two ranges: one that encloses its definition and one that points to * its most interesting range, e.g. the range of an identifier. */ export interface DocumentSymbol { /** * The name of this symbol. Will be displayed in the user interface and therefore must not be * an empty string or a string only consisting of white spaces. */ name: string /** * More detail for this symbol, e.g the signature of a function. */ detail?: string /** * The kind of this symbol. */ kind: SymbolKind /** * Tags for this document symbol. * * @since 3.16.0 */ tags?: SymbolTag[] /** * Indicates if this symbol is deprecated. * * @deprecated Use tags instead */ deprecated?: boolean /** * The range enclosing this symbol not including leading/trailing whitespace but everything else * like comments. This information is typically used to determine if the clients cursor is * inside the symbol to reveal in the symbol in the UI. */ range: Range /** * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. * Must be contained by the `range`. */ selectionRange: Range /** * Children of this symbol, e.g. properties of a class. */ children?: DocumentSymbol[] } export namespace DocumentSymbol { /** * Creates a new symbol information literal. * * @param name The name of the symbol. * @param detail The detail of the symbol. * @param kind The kind of the symbol. * @param range The range of the symbol. * @param selectionRange The selectionRange of the symbol. * @param children Children of the symbol. */ function create(name: string, detail: string | undefined, kind: SymbolKind, range: Range, selectionRange: Range, children?: DocumentSymbol[]): DocumentSymbol /** * Checks whether the given literal conforms to the [DocumentSymbol](#DocumentSymbol) interface. */ function is(value: any): value is DocumentSymbol } /** * The kind of a code action. * * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. * * The set of kinds is open and client needs to announce the kinds it supports to the server during * initialization. */ export type CodeActionKind = string /** * A set of predefined code action kinds */ export namespace CodeActionKind { /** * Empty kind. */ const Empty: '' /** * Base kind for quickfix actions: 'quickfix' */ const QuickFix: 'quickfix' /** * Base kind for refactoring actions: 'refactor' */ const Refactor: 'refactor' /** * Base kind for refactoring extraction actions: 'refactor.extract' * * Example extract actions: * * - Extract method * - Extract function * - Extract variable * - Extract interface from class * - ... */ const RefactorExtract: 'refactor.extract' /** * Base kind for refactoring inline actions: 'refactor.inline' * * Example inline actions: * * - Inline function * - Inline variable * - Inline constant * - ... */ const RefactorInline: 'refactor.inline' /** * Base kind for refactoring rewrite actions: 'refactor.rewrite' * * Example rewrite actions: * * - Convert JavaScript function to class * - Add or remove parameter * - Encapsulate field * - Make method static * - Move method to base class * - ... */ const RefactorRewrite: 'refactor.rewrite' /** * Base kind for source actions: `source` * * Source code actions apply to the entire file. */ const Source: 'source' /** * Base kind for an organize imports source action: `source.organizeImports` */ const SourceOrganizeImports: 'source.organizeImports' /** * Base kind for auto-fix source actions: `source.fixAll`. * * Fix all actions automatically fix errors that have a clear fix that do not require user input. * They should not suppress errors or perform unsafe fixes such as generating new types or classes. * * @since 3.15.0 */ const SourceFixAll: 'source.fixAll' } /** * The reason why code actions were requested. * * @since 3.17.0 */ export namespace CodeActionTriggerKind { /** * Code actions were explicitly requested by the user or by an extension. */ const Invoked: 1 /** * Code actions were requested automatically. * * This typically happens when current selection in a file changes, but can * also be triggered when file content changes. */ const Automatic: 2 } export type CodeActionTriggerKind = 1 | 2 /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. */ export interface CodeActionContext { /** * An array of diagnostics known on the client side overlapping the range provided to the * `textDocument/codeAction` request. They are provided so that the server knows which * errors are currently presented to the user for the given range. There is no guarantee * that these accurately reflect the error state of the resource. The primary parameter * to compute code actions is the provided range. */ diagnostics: Diagnostic[] /** * Requested kind of actions to return. * * Actions not of this kind are filtered out by the client before being shown. So servers * can omit computing them. */ only?: CodeActionKind[] /** * The reason why code actions were requested. * * @since 3.17.0 */ triggerKind?: CodeActionTriggerKind } /** * The CodeActionContext namespace provides helper functions to work with * [CodeActionContext](#CodeActionContext) literals. */ export namespace CodeActionContext { /** * Creates a new CodeActionContext literal. */ function create(diagnostics: Diagnostic[], only?: CodeActionKind[], triggerKind?: CodeActionTriggerKind): CodeActionContext /** * Checks whether the given literal conforms to the [CodeActionContext](#CodeActionContext) interface. */ function is(value: any): value is CodeActionContext } /** * A code action represents a change that can be performed in code, e.g. to fix a problem or * to refactor code. * * A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. */ export interface CodeAction { /** * A short, human-readable, title for this code action. */ title: string /** * The kind of the code action. * * Used to filter code actions. */ kind?: CodeActionKind /** * The diagnostics that this code action resolves. */ diagnostics?: Diagnostic[] /** * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted * by keybindings. * * A quick fix should be marked preferred if it properly addresses the underlying error. * A refactoring should be marked preferred if it is the most reasonable choice of actions to take. * * @since 3.15.0 */ isPreferred?: boolean /** * Marks that the code action cannot currently be applied. * * Clients should follow the following guidelines regarding disabled code actions: * * - Disabled code actions are not shown in automatic [lightbulbs](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) * code action menus. * * - Disabled actions are shown as faded out in the code action menu when the user requests a more specific type * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) * that auto applies a code action and only disabled code actions are returned, the client should show the user an * error message with `reason` in the editor. * * @since 3.16.0 */ disabled?: { /** * Human readable description of why the code action is currently disabled. * * This is displayed in the code actions UI. */ reason: string } /** * The workspace edit this code action performs. */ edit?: WorkspaceEdit /** * A command this code action executes. If a code action * provides an edit and a command, first the edit is * executed and then the command. */ command?: Command /** * A data entry field that is preserved on a code action between * a `textDocument/codeAction` and a `codeAction/resolve` request. * * @since 3.16.0 */ data?: LSPAny } export namespace CodeAction { /** * Creates a new code action. * * @param title The title of the code action. * @param kind The kind of the code action. */ function create(title: string, kind?: CodeActionKind): CodeAction /** * Creates a new code action. * * @param title The title of the code action. * @param command The command to execute. * @param kind The kind of the code action. */ function create(title: string, command: Command, kind?: CodeActionKind): CodeAction /** * Creates a new code action. * * @param title The title of the code action. * @param edit The edit to perform. * @param kind The kind of the code action. */ function create(title: string, edit: WorkspaceEdit, kind?: CodeActionKind): CodeAction function is(value: any): value is CodeAction } /** * A code lens represents a [command](#Command) that should be shown along with * source text, like the number of references, a way to run tests, etc. * * A code lens is _unresolved_ when no command is associated to it. For performance * reasons the creation of a code lens and resolving should be done in two stages. */ export interface CodeLens { /** * The range in which this code lens is valid. Should only span a single line. */ range: Range /** * The command this code lens represents. */ command?: Command /** * A data entry field that is preserved on a code lens item between * a [CodeLensRequest](#CodeLensRequest) and a [CodeLensResolveRequest] * (#CodeLensResolveRequest) */ data?: LSPAny } /** * The CodeLens namespace provides helper functions to work with * [CodeLens](#CodeLens) literals. */ export namespace CodeLens { /** * Creates a new CodeLens literal. */ function create(range: Range, data?: LSPAny): CodeLens /** * Checks whether the given literal conforms to the [CodeLens](#CodeLens) interface. */ function is(value: any): value is CodeLens } /** * Value-object describing what options formatting should use. */ export interface FormattingOptions { /** * Size of a tab in spaces. */ tabSize: uinteger /** * Prefer spaces over tabs. */ insertSpaces: boolean /** * Trim trailing whitespace on a line. * * @since 3.15.0 */ trimTrailingWhitespace?: boolean /** * Insert a newline character at the end of the file if one does not exist. * * @since 3.15.0 */ insertFinalNewline?: boolean /** * Trim all newlines after the final newline at the end of the file. * * @since 3.15.0 */ trimFinalNewlines?: boolean /** * Signature for further properties. */ [key: string]: boolean | integer | string | undefined } /** * The FormattingOptions namespace provides helper functions to work with * [FormattingOptions](#FormattingOptions) literals. */ export namespace FormattingOptions { /** * Creates a new FormattingOptions literal. */ function create(tabSize: uinteger, insertSpaces: boolean): FormattingOptions /** * Checks whether the given literal conforms to the [FormattingOptions](#FormattingOptions) interface. */ function is(value: any): value is FormattingOptions } /** * A document link is a range in a text document that links to an internal or external resource, like another * text document or a web site. */ export interface DocumentLink { /** * The range this link applies to. */ range: Range /** * The uri this link points to. If missing a resolve request is sent later. */ target?: string /** * The tooltip text when you hover over this link. * * If a tooltip is provided, is will be displayed in a string that includes instructions on how to * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, * user settings, and localization. * * @since 3.15.0 */ tooltip?: string /** * A data entry field that is preserved on a document link between a * DocumentLinkRequest and a DocumentLinkResolveRequest. */ data?: LSPAny } /** * The DocumentLink namespace provides helper functions to work with * [DocumentLink](#DocumentLink) literals. */ export namespace DocumentLink { /** * Creates a new DocumentLink literal. */ function create(range: Range, target?: string, data?: LSPAny): DocumentLink /** * Checks whether the given literal conforms to the [DocumentLink](#DocumentLink) interface. */ function is(value: any): value is DocumentLink } /** * A selection range represents a part of a selection hierarchy. A selection range * may have a parent selection range that contains it. */ export interface SelectionRange { /** * The [range](#Range) of this selection range. */ range: Range /** * The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. */ parent?: SelectionRange } /** * The SelectionRange namespace provides helper function to work with * SelectionRange literals. */ export namespace SelectionRange { /** * Creates a new SelectionRange * @param range the range. * @param parent an optional parent. */ function create(range: Range, parent?: SelectionRange): SelectionRange function is(value: any): value is SelectionRange } /** * Represents programming constructs like functions or constructors in the context * of call hierarchy. * * @since 3.16.0 */ export interface CallHierarchyItem { /** * The name of this item. */ name: string /** * The kind of this item. */ kind: SymbolKind /** * Tags for this item. */ tags?: SymbolTag[] /** * More detail for this item, e.g. the signature of a function. */ detail?: string /** * The resource identifier of this item. */ uri: DocumentUri /** * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. */ range: Range /** * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. * Must be contained by the [`range`](#CallHierarchyItem.range). */ selectionRange: Range /** * A data entry field that is preserved between a call hierarchy prepare and * incoming calls or outgoing calls requests. */ data?: LSPAny } /** * Represents an incoming call, e.g. a caller of a method or constructor. * * @since 3.16.0 */ export interface CallHierarchyIncomingCall { /** * The item that makes the call. */ from: CallHierarchyItem /** * The ranges at which the calls appear. This is relative to the caller * denoted by [`this.from`](#CallHierarchyIncomingCall.from). */ fromRanges: Range[] } /** * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. * * @since 3.16.0 */ export interface CallHierarchyOutgoingCall { /** * The item that is called. */ to: CallHierarchyItem /** * The range at which this item is called. This is the range relative to the caller, e.g the item * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) * and not [`this.to`](#CallHierarchyOutgoingCall.to). */ fromRanges: Range[] } /** * A set of predefined token types. This set is not fixed * an clients can specify additional token types via the * corresponding client capabilities. * * @since 3.16.0 */ export enum SemanticTokenTypes { namespace = "namespace", /** * Represents a generic type. Acts as a fallback for types which can't be mapped to * a specific type like class or enum. */ type = "type", class = "class", enum = "enum", interface = "interface", struct = "struct", typeParameter = "typeParameter", parameter = "parameter", variable = "variable", property = "property", enumMember = "enumMember", event = "event", function = "function", method = "method", macro = "macro", keyword = "keyword", modifier = "modifier", comment = "comment", string = "string", number = "number", regexp = "regexp", operator = "operator", /** * @since 3.17.0 */ decorator = "decorator" } /** * A set of predefined token modifiers. This set is not fixed * an clients can specify additional token types via the * corresponding client capabilities. * * @since 3.16.0 */ export enum SemanticTokenModifiers { declaration = "declaration", definition = "definition", readonly = "readonly", static = "static", deprecated = "deprecated", abstract = "abstract", async = "async", modification = "modification", documentation = "documentation", defaultLibrary = "defaultLibrary" } /** * @since 3.16.0 */ export interface SemanticTokensLegend { /** * The token types a server uses. */ tokenTypes: string[] /** * The token modifiers a server uses. */ tokenModifiers: string[] } /** * @since 3.16.0 */ export interface SemanticTokens { /** * An optional result id. If provided and clients support delta updating * the client will include the result id in the next semantic token request. * A server can then instead of computing all semantic tokens again simply * send a delta. */ resultId?: string /** * The actual tokens. */ data: uinteger[] } /** * @since 3.16.0 */ export namespace SemanticTokens { function is(value: any): value is SemanticTokens } /** * @since 3.16.0 */ export interface SemanticTokensEdit { /** * The start offset of the edit. */ start: uinteger /** * The count of elements to remove. */ deleteCount: uinteger /** * The elements to insert. */ data?: uinteger[] } /** * @since 3.16.0 */ export interface SemanticTokensDelta { readonly resultId?: string /** * The semantic token edits to transform a previous result into a new result. */ edits: SemanticTokensEdit[] } /** * @since 3.17.0 */ export type TypeHierarchyItem = { /** * The name of this item. */ name: string /** * The kind of this item. */ kind: SymbolKind /** * Tags for this item. */ tags?: SymbolTag[] /** * More detail for this item, e.g. the signature of a function. */ detail?: string /** * The resource identifier of this item. */ uri: DocumentUri /** * The range enclosing this symbol not including leading/trailing whitespace * but everything else, e.g. comments and code. */ range: Range /** * The range that should be selected and revealed when this symbol is being * picked, e.g. the name of a function. Must be contained by the * [`range`](#TypeHierarchyItem.range). */ selectionRange: Range /** * A data entry field that is preserved between a type hierarchy prepare and * supertypes or subtypes requests. It could also be used to identify the * type hierarchy in the server, helping improve the performance on * resolving supertypes and subtypes. */ data?: LSPAny } /** * Provide inline value as text. * * @since 3.17.0 */ export type InlineValueText = { /** * The document range for which the inline value applies. */ range: Range /** * The text of the inline value. */ text: string } /** * The InlineValueText namespace provides functions to deal with InlineValueTexts. * * @since 3.17.0 */ export namespace InlineValueText { /** * Creates a new InlineValueText literal. */ function create(range: Range, text: string): InlineValueText function is(value: InlineValue | undefined | null): value is InlineValueText } /** * Provide inline value through a variable lookup. * If only a range is specified, the variable name will be extracted from the underlying document. * An optional variable name can be used to override the extracted name. * * @since 3.17.0 */ export type InlineValueVariableLookup = { /** * The document range for which the inline value applies. * The range is used to extract the variable name from the underlying document. */ range: Range /** * If specified the name of the variable to look up. */ variableName?: string /** * How to perform the lookup. */ caseSensitiveLookup: boolean } /** * The InlineValueVariableLookup namespace provides functions to deal with InlineValueVariableLookups. * * @since 3.17.0 */ export namespace InlineValueVariableLookup { /** * Creates a new InlineValueText literal. */ function create(range: Range, variableName: string | undefined, caseSensitiveLookup: boolean): InlineValueVariableLookup function is(value: InlineValue | undefined | null): value is InlineValueVariableLookup } /** * Provide an inline value through an expression evaluation. * If only a range is specified, the expression will be extracted from the underlying document. * An optional expression can be used to override the extracted expression. * * @since 3.17.0 */ export type InlineValueEvaluatableExpression = { /** * The document range for which the inline value applies. * The range is used to extract the evaluatable expression from the underlying document. */ range: Range /** * If specified the expression overrides the extracted expression. */ expression?: string } /** * The InlineValueEvaluatableExpression namespace provides functions to deal with InlineValueEvaluatableExpression. * * @since 3.17.0 */ export namespace InlineValueEvaluatableExpression { /** * Creates a new InlineValueEvaluatableExpression literal. */ function create(range: Range, expression: string | undefined): InlineValueEvaluatableExpression function is(value: InlineValue | undefined | null): value is InlineValueEvaluatableExpression } /** * Inline value information can be provided by different means: * - directly as a text value (class InlineValueText). * - as a name to use for a variable lookup (class InlineValueVariableLookup) * - as an evaluatable expression (class InlineValueEvaluatableExpression) * The InlineValue types combines all inline value types into one type. * * @since 3.17.0 */ export type InlineValue = InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression /** * @since 3.17.0 */ export type InlineValueContext = { /** * The stack frame (as a DAP Id) where the execution has stopped. */ frameId: integer /** * The document range where execution has stopped. * Typically the end position of the range denotes the line where the inline values are shown. */ stoppedLocation: Range } /** * The InlineValueContext namespace provides helper functions to work with * [InlineValueContext](#InlineValueContext) literals. * * @since 3.17.0 */ export namespace InlineValueContext { /** * Creates a new InlineValueContext literal. */ function create(frameId: integer, stoppedLocation: Range): InlineValueContext /** * Checks whether the given literal conforms to the [InlineValueContext](#InlineValueContext) interface. */ function is(value: any): value is InlineValueContext } /** * Inlay hint kinds. * * @since 3.17.0 */ export namespace InlayHintKind { /** * An inlay hint that for a type annotation. */ const Type = 1 /** * An inlay hint that is for a parameter. */ const Parameter = 2 function is(value: number): value is InlayHintKind } export type InlayHintKind = 1 | 2 /** * An inlay hint label part allows for interactive and composite labels * of inlay hints. * * @since 3.17.0 */ export type InlayHintLabelPart = { /** * The value of this label part. */ value: string /** * The tooltip text when you hover over this label part. Depending on * the client capability `inlayHint.resolveSupport` clients might resolve * this property late using the resolve request. */ tooltip?: string | MarkupContent /** * An optional source code location that represents this * label part. * * The editor will use this location for the hover and for code navigation * features: This part will become a clickable link that resolves to the * definition of the symbol at the given location (not necessarily the * location itself), it shows the hover that shows at the given location, * and it shows a context menu with further code navigation commands. * * Depending on the client capability `inlayHint.resolveSupport` clients * might resolve this property late using the resolve request. */ location?: Location /** * An optional command for this label part. * * Depending on the client capability `inlayHint.resolveSupport` clients * might resolve this property late using the resolve request. */ command?: Command } export namespace InlayHintLabelPart { function create(value: string): InlayHintLabelPart function is(value: any): value is InlayHintLabelPart } /** * Inlay hint information. * * @since 3.17.0 */ export type InlayHint = { /** * The position of this hint. */ position: Position /** * The label of this hint. A human readable string or an array of * InlayHintLabelPart label parts. * * *Note* that neither the string nor the label part can be empty. */ label: string | InlayHintLabelPart[] /** * The kind of this hint. Can be omitted in which case the client * should fall back to a reasonable default. */ kind?: InlayHintKind /** * Optional text edits that are performed when accepting this inlay hint. * * *Note* that edits are expected to change the document so that the inlay * hint (or its nearest variant) is now part of the document and the inlay * hint itself is now obsolete. */ textEdits?: TextEdit[] /** * The tooltip text when you hover over this item. */ tooltip?: string | MarkupContent /** * Render padding before the hint. * * Note: Padding should use the editor's background color, not the * background color of the hint itself. That means padding can be used * to visually align/separate an inlay hint. */ paddingLeft?: boolean /** * Render padding after the hint. * * Note: Padding should use the editor's background color, not the * background color of the hint itself. That means padding can be used * to visually align/separate an inlay hint. */ paddingRight?: boolean /** * A data entry field that is preserved on an inlay hint between * a `textDocument/inlayHint` and a `inlayHint/resolve` request. */ data?: LSPAny } export namespace InlayHint { function create(position: Position, label: string | InlayHintLabelPart[], kind?: InlayHintKind): InlayHint function is(value: any): value is InlayHint } /** * A workspace folder inside a client. */ export interface WorkspaceFolder { /** * The associated URI for this workspace folder. */ uri: string /** * The name of the workspace folder. Used to refer to this * workspace folder in the user interface. */ name: string } export namespace WorkspaceFolder { function is(value: any): value is WorkspaceFolder } export const EOL: string[] /** * A simple text document. Not to be implemented. The document keeps the content * as string. */ export interface TextDocument { /** * The associated URI for this document. Most documents have the __file__-scheme, indicating that they * represent files on disk. However, some documents may have other schemes indicating that they are not * available on disk. * * @readonly */ readonly uri: DocumentUri /** * The identifier of the language associated with this document. * * @readonly */ readonly languageId: string /** * The version number of this document (it will increase after each * change, including undo/redo). * * @readonly */ readonly version: integer /** * Get the text of this document. A substring can be retrieved by * providing a range. * * @param range (optional) An range within the document to return. * If no range is passed, the full content is returned. * Invalid range positions are adjusted as described in [Position.line](#Position.line) * and [Position.character](#Position.character). * If the start range position is greater than the end range position, * then the effect of getText is as if the two positions were swapped. * @return The text of this document or a substring of the text if a * range is provided. */ getText(range?: Range): string /** * Converts a zero-based offset to a position. * * @param offset A zero-based offset. * @return A valid [position](#Position). */ positionAt(offset: uinteger): Position /** * Converts the position to a zero-based offset. * Invalid positions are adjusted as described in [Position.line](#Position.line) * and [Position.character](#Position.character). * * @param position A position. * @return A valid zero-based offset. */ offsetAt(position: Position): uinteger /** * The number of lines in this document. * * @readonly */ readonly lineCount: uinteger } /** * Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. */ export namespace InlineCompletionTriggerKind { /** * Completion was triggered explicitly by a user gesture. */ const Invoked: 1 /** * Completion was triggered automatically while editing. */ const Automatic: 2 } export interface InlineCompletionOption { /** * The provider name, extension name or LanguageClient id. */ provider?: string /** * Set trigger kind to InlineCompletionTriggerKind.Automatic when true. */ autoTrigger?: boolean } export type InlineCompletionTriggerKind = 1 | 2 /** * Describes the currently selected completion item. */ export type SelectedCompletionInfo = { /** * The range that will be replaced if this completion item is accepted. */ range: Range /** * The text the range will be replaced with if this completion is accepted. */ text: string } export namespace SelectedCompletionInfo { function create(range: Range, text: string): SelectedCompletionInfo } /** * Provides information about the context in which an inline completion was requested. */ export type InlineCompletionContext = { /** * Describes how the inline completion was triggered. */ triggerKind: InlineCompletionTriggerKind /** * Provides information about the currently selected item in the autocomplete widget if it is visible. */ selectedCompletionInfo?: SelectedCompletionInfo } export namespace InlineCompletionContext { function create(triggerKind: InlineCompletionTriggerKind, selectedCompletionInfo?: SelectedCompletionInfo): InlineCompletionContext } /** * A string value used as a snippet is a template which allows to insert text * and to control the editor cursor when insertion happens. * * A snippet can define tab stops and placeholders with `$1`, `$2` * and `${3:foo}`. `$0` defines the final tab stop, it defaults to * the end of the snippet. Variables are defined with `$name` and * `${name:default value}`. */ export type StringValue = { /** * The kind of string value. */ kind: 'snippet' /** * The snippet string. */ value: string } export namespace StringValue { function createSnippet(value: string): StringValue function isSnippet(value: any): value is StringValue } /** * An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. */ export interface InlineCompletionItem { /** * The text to replace the range with. Must be set. */ insertText: string | StringValue /** * A text that is used to decide if this inline completion should be shown. When `falsy` the {@link InlineCompletionItem.insertText} is used. */ filterText?: string /** * The range to replace. Must begin and end on the same line. */ range?: Range /** * An optional {@link Command} that is executed *after* inserting this completion. */ command?: Command } export namespace InlineCompletionItem { function create(insertText: string | StringValue, filterText?: string, range?: Range, command?: Command): InlineCompletionItem } /** * Represents a collection of {@link InlineCompletionItem inline completion items} to be presented in the editor. */ export interface InlineCompletionList { /** * The inline completion items */ items: InlineCompletionItem[] } export namespace InlineCompletionList { function create(items: InlineCompletionItem[]): InlineCompletionList } /** * An interactive text edit. */ export interface SnippetTextEdit { /** * The range of the text document to be manipulated. */ range: Range /** * The snippet to be inserted. */ snippet: StringValue /** * The actual identifier of the snippet edit. */ annotationId?: ChangeAnnotationIdentifier } export namespace SnippetTextEdit { function is(value: any): value is SnippetTextEdit } /** * Defines how values from a set of defaults and an individual item will be * merged. */ export namespace ApplyKind { /** * The value from the individual item (if provided and not `null`) will be * used instead of the default. */ const Replace: 1 /** * The value from the item will be merged with the default. * * The specific rules for merging values are defined against each field * that supports merging. */ const Merge: 2 } /** * Defines how values from a set of defaults and an individual item will be * merged. */ export type ApplyKind = 1 | 2 /** * Additional data about a workspace edit. */ export type WorkspaceEditMetadata = { /** * Signal to the editor that this edit is a refactoring. */ isRefactoring?: boolean } /** * The parameters passed via an apply workspace edit request. */ export interface ApplyWorkspaceEditParams { /** * An optional label of the workspace edit. This label is * presented in the user interface for example on an undo * stack to undo the workspace edit. */ label?: string /** * The edits to apply. */ edit: WorkspaceEdit /** * Additional data about the edit. * * @since 3.18.0 * @proposed */ metadata?: WorkspaceEditMetadata } /** * The result returned from the apply workspace edit request. */ export interface ApplyWorkspaceEditResult { /** * Indicates whether the edit was applied or not. */ applied: boolean /** * An optional textual description for why the edit was not applied. * This may be used by the server for diagnostic logging or to provide * a suitable error for a request that triggered the edit. */ failureReason?: string /** * Depending on the client's failure handling strategy `failedChange` might * contain the index of the change that failed. This property is only available * if the client signals a `failureHandlingStrategy` in its client capabilities. */ failedChange?: uinteger } // }} // Language server protocol interfaces {{ export interface Thenable { then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable // eslint-disable-next-line @typescript-eslint/unified-signatures then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable } export interface Disposable { /** * Dispose this object. */ dispose(): void } export namespace Disposable { function create(func: () => void): Disposable } /** * A parameter literal used in requests to pass a text document and a position inside that * document. */ export interface TextDocumentPositionParams { /** * The text document. */ textDocument: TextDocumentIdentifier /** * The position inside the text document. */ position: Position } /** * An event describing a change to a text document. */ export interface TextDocumentContentChange { /** * The range of the document that changed. */ range: Range /** * The new text for the provided range. */ text: string } /** * The workspace folder change event. */ export interface WorkspaceFoldersChangeEvent { /** * The array of added workspace folders */ added: WorkspaceFolder[] /** * The array of the removed workspace folders */ removed: WorkspaceFolder[] } /** * An event that is fired when a [document](#LinesTextDocument) will be saved. * * To make modifications to the document before it is being saved, call the * [`waitUntil`](#TextDocumentWillSaveEvent.waitUntil)-function with a thenable * that resolves to an array of [text edits](#TextEdit). */ export interface TextDocumentWillSaveEvent { /** * The document that will be saved. */ document: LinesTextDocument /** * The reason why save was triggered. */ reason: 1 | 2 | 3 } /** * A document filter denotes a document by different properties like * the [language](#LinesTextDocument.languageId), the [scheme](#Uri.scheme) of * its resource, or a glob-pattern that is applied to the [path](#LinesTextDocument.fileName). * * Glob patterns can have the following syntax: * - `*` to match one or more characters in a path segment * - `?` to match on one character in a path segment * - `**` to match any number of path segments, including none * - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) * * @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }` */ export type DocumentFilter = { /** A language id, like `typescript`. */ language: string /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */ scheme?: string /** A glob pattern, like `*.{ts,js}`. */ pattern?: string } | { /** A language id, like `typescript`. */ language?: string /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */ scheme: string /** A glob pattern, like `*.{ts,js}`. */ pattern?: string } | { /** A language id, like `typescript`. */ language?: string /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */ scheme?: string /** A glob pattern, like `*.{ts,js}`. */ pattern: string } /** * A language selector is the combination of one or many language identifiers * and {@link DocumentFilter language filters}. * * *Note* that a document selector that is just a language identifier selects *all* * documents, even those that are not saved on disk. Only use such selectors when * a feature works without further context, e.g. without the need to resolve related * 'files'. * * @example * let sel:DocumentSelector = { scheme: 'file', language: 'typescript' }; */ export type DocumentSelector = DocumentFilter | string | ReadonlyArray /** * How a signature help was triggered. */ export namespace SignatureHelpTriggerKind { /** * Signature help was invoked manually by the user or by a command. */ const Invoked: 1 /** * Signature help was triggered by a trigger character. */ const TriggerCharacter: 2 /** * Signature help was triggered by the cursor moving or by the document content changing. */ const ContentChange: 3 } export type SignatureHelpTriggerKind = 1 | 2 | 3 /** * Additional information about the context in which a signature help request was triggered. * * @since 3.15.0 */ export interface SignatureHelpContext { /** * Action that caused signature help to be triggered. */ triggerKind: SignatureHelpTriggerKind /** * Character that caused signature help to be triggered. * * This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` */ triggerCharacter?: string /** * `true` if signature help was already showing when it was triggered. * * Retriggers occur when the signature help is already active and can be caused by actions such as * typing a trigger character, a cursor move, or document content changes. */ isRetrigger: boolean /** * The currently active `SignatureHelp`. * * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on * the user navigating through available signatures. */ activeSignatureHelp?: SignatureHelp } /** * How a completion was triggered */ export namespace CompletionTriggerKind { /** * Completion was triggered by typing an identifier (24x7 code * complete), manual invocation (e.g Ctrl+Space) or via API. */ const Invoked: 1 /** * Completion was triggered by a trigger character specified by * the `triggerCharacters` properties of the `CompletionRegistrationOptions`. */ const TriggerCharacter: 2 /** * Completion was re-triggered as current completion list is incomplete */ const TriggerForIncompleteCompletions: 3 } export type CompletionTriggerKind = 1 | 2 | 3 /** * Contains additional information about the context in which a completion request is triggered. */ export interface CompletionContext { /** * How the completion was triggered. */ triggerKind: CompletionTriggerKind, /** * The trigger character (a single character) that has trigger code complete. * Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` */ triggerCharacter?: string option: CompleteOption } /** * Represents a typed event. * * A function that represents an event to which you subscribe by calling it with * a listener function as argument. * * @example * item.onDidChange(function(event) { console.log("Event happened: " + event); }); */ export interface Event { /** * A function that represents an event to which you subscribe by calling it with * a listener function as argument. * * @param listener The listener function will be called when the event happens. * @param thisArgs The `this`-argument which will be used when calling the event listener. * @param disposables An array to which a [disposable](#Disposable) will be added. * @return A disposable which unsubscribes the event listener. */ (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable } export namespace Event { const None: Event } export interface EmitterOptions { onFirstListenerAdd?: Function onLastListenerRemove?: Function } export class Emitter { constructor(_options?: EmitterOptions | undefined) /** * For the public to allow to subscribe * to events from this Emitter */ get event(): Event /** * To be kept private to fire an event to * subscribers */ fire(event: T): any dispose(): void } /** * Defines a CancellationToken. This interface is not * intended to be implemented. A CancellationToken must * be created via a CancellationTokenSource. */ export interface CancellationToken { /** * Is `true` when the token has been cancelled, `false` otherwise. */ readonly isCancellationRequested: boolean /** * An [event](#Event) which fires upon cancellation. */ readonly onCancellationRequested: Event } export namespace CancellationToken { const None: CancellationToken const Cancelled: CancellationToken function is(value: any): value is CancellationToken } export class CancellationTokenSource { get token(): CancellationToken cancel(): void dispose(): void } /** * Represents a line of text, such as a line of source code. * * TextLine objects are __immutable__. When a {@link LinesTextDocument document} changes, * previously retrieved lines will not represent the latest state. */ export interface TextLine { /** * The zero-based line number. */ readonly lineNumber: number /** * The text of this line without the line separator characters. */ readonly text: string /** * The range this line covers without the line separator characters. */ readonly range: Range /** * The range this line covers with the line separator characters. */ readonly rangeIncludingLineBreak: Range /** * The offset of the first character which is not a whitespace character as defined * by `/\s/`. **Note** that if a line is all whitespace the length of the line is returned. */ readonly firstNonWhitespaceCharacterIndex: number /** * Whether this line is whitespace only, shorthand * for {@link TextLine.firstNonWhitespaceCharacterIndex} === {@link TextLine.text TextLine.text.length}. */ readonly isEmptyOrWhitespace: boolean } export interface LinesTextDocument extends TextDocument { /** * Total length of TextDocument. */ readonly length: number /** * End position of TextDocument. */ readonly end: Position /** * 'eol' option of related buffer. When enabled additional `\n` will be * added to the end of document content */ readonly eol: boolean /** * Lines of TextDocument. */ readonly lines: ReadonlyArray /** * Returns a text line denoted by the line number. Note * that the returned object is *not* live and changes to the * document are not reflected. * * @param line or position * @return A {@link TextLine line}. */ lineAt(lineOrPos: number | Position): TextLine } /** * The result of a linked editing range request. * * @since 3.16.0 */ export interface LinkedEditingRanges { /** * A list of ranges that can be edited together. The ranges must have * identical length and contain identical text content. The ranges cannot overlap. */ ranges: Range[] /** * An optional word pattern (regular expression) that describes valid contents for * the given ranges. If no pattern is provided, the client configuration's word * pattern will be used. */ wordPattern?: string } /** * Moniker uniqueness level to define scope of the moniker. * * @since 3.16.0 */ export namespace UniquenessLevel { /** * The moniker is only unique inside a document */ export const document = 'document' /** * The moniker is unique inside a project for which a dump got created */ export const project = 'project' /** * The moniker is unique inside the group to which a project belongs */ export const group = 'group' /** * The moniker is unique inside the moniker scheme. */ export const scheme = 'scheme' /** * The moniker is globally unique */ export const global = 'global' } export type UniquenessLevel = 'document' | 'project' | 'group' | 'scheme' | 'global' /** * The moniker kind. * * @since 3.16.0 */ export enum MonikerKind { /** * The moniker represent a symbol that is imported into a project */ import = 'import', /** * The moniker represents a symbol that is exported from a project */ export = 'export', /** * The moniker represents a symbol that is local to a project (e.g. a local * variable of a function, a class not visible outside the project, ...) */ local = 'local' } /** * Moniker definition to match LSIF 0.5 moniker definition. * * @since 3.16.0 */ export interface Moniker { /** * The scheme of the moniker. For example tsc or .Net */ scheme: string /** * The identifier of the moniker. The value is opaque in LSIF however * schema owners are allowed to define the structure if they want. */ identifier: string /** * The scope in which the moniker is unique */ unique: UniquenessLevel /** * The moniker kind if known. */ kind?: MonikerKind } /** * A previous result id in a workspace pull request. * * @since 3.17.0 */ export type PreviousResultId = { /** * The URI for which the client knowns a * result id. */ uri: string /** * The value of the previous result id. */ value: string } export type DocumentDiagnosticReportKind = 'full' | 'unchanged' /** * The document diagnostic report kinds. * * @since 3.17.0 */ export namespace DocumentDiagnosticReportKind { /** * A diagnostic report with a full * set of problems. */ const Full = "full" /** * A report indicating that the last * returned report is still accurate. */ const Unchanged = "unchanged" } /** * A diagnostic report with a full set of problems. * * @since 3.17.0 */ export type FullDocumentDiagnosticReport = { /** * A full document diagnostic report. */ kind: typeof DocumentDiagnosticReportKind.Full /** * An optional result id. If provided it will * be sent on the next diagnostic request for the * same document. */ resultId?: string /** * The actual items. */ items: Diagnostic[] } /** * A diagnostic report indicating that the last returned * report is still accurate. * * @since 3.17.0 */ export type UnchangedDocumentDiagnosticReport = { /** * A document diagnostic report indicating * no changes to the last result. A server can * only return `unchanged` if result ids are * provided. */ kind: typeof DocumentDiagnosticReportKind.Unchanged /** * A result id which will be sent on the next * diagnostic request for the same document. */ resultId: string } /** * An unchanged diagnostic report with a set of related documents. * * @since 3.17.0 */ export type RelatedUnchangedDocumentDiagnosticReport = UnchangedDocumentDiagnosticReport & { /** * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate * diagnostics in a file B which A depends on. An example of * such a language is C/C++ where marco definitions in a file * a.cpp and result in errors in a header file b.hpp. * * @since 3.17.0 */ relatedDocuments?: { [uri: string]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport } } export type RelatedFullDocumentDiagnosticReport = FullDocumentDiagnosticReport & { /** * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate * diagnostics in a file B which A depends on. An example of * such a language is C/C++ where marco definitions in a file * a.cpp and result in errors in a header file b.hpp. * * @since 3.17.0 */ relatedDocuments?: { [uri: string]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport } } export type DocumentDiagnosticReport = RelatedFullDocumentDiagnosticReport | RelatedUnchangedDocumentDiagnosticReport /* * A workspace diagnostic report. * * @since 3.17.0 */ export type WorkspaceDiagnosticReport = { items: WorkspaceDocumentDiagnosticReport[] } /** * A partial result for a workspace diagnostic report. * * @since 3.17.0 */ export type WorkspaceDiagnosticReportPartialResult = { items: WorkspaceDocumentDiagnosticReport[] } export type WorkspaceFullDocumentDiagnosticReport = FullDocumentDiagnosticReport & { /** * The URI for which diagnostic information is reported. */ uri: string /** * The version number for which the diagnostics are reported. * If the document is not marked as open `null` can be provided. */ version: number | null } export type WorkspaceUnchangedDocumentDiagnosticReport = UnchangedDocumentDiagnosticReport & { /** * The URI for which diagnostic information is reported. */ uri: string /** * The version number for which the diagnostics are reported. * If the document is not marked as open `null` can be provided. */ version: number | null } export type WorkspaceDocumentDiagnosticReport = WorkspaceFullDocumentDiagnosticReport | WorkspaceUnchangedDocumentDiagnosticReport export interface ResultReporter { (chunk: WorkspaceDiagnosticReportPartialResult | null): void } export type ErrorCodes = number /** * Predefined error codes. */ export namespace ErrorCodes { const ParseError: -32700 const InvalidRequest: -32600 const MethodNotFound: -32601 const InvalidParams: -32602 const InternalError: -32603 /** * This is the start range of JSON RPC reserved error codes. * It doesn't denote a real error code. No application error codes should * be defined between the start and end range. For backwards * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` * are left in the range. * * @since 3.16.0 */ const jsonrpcReservedErrorRangeStart: -32099 /** @deprecated use jsonrpcReservedErrorRangeStart */ const serverErrorStart: -32099 /** * An error occurred when write a message to the transport layer. */ const MessageWriteError: -32099 /** * An error occurred when reading a message from the transport layer. */ const MessageReadError: -32098 /** * The connection got disposed or lost and all pending responses got * rejected. */ const PendingResponseRejected: -32097 /** * The connection is inactive and a use of it failed. */ const ConnectionInactive: -32096 /** * Error code indicating that a server received a notification or * request before the server has received the `initialize` request. */ const ServerNotInitialized: -32002 const UnknownErrorCode: -32001 /** * This is the end range of JSON RPC reserved error codes. * It doesn't denote a real error code. * * @since 3.16.0 */ const jsonrpcReservedErrorRangeEnd: -32000 /** @deprecated use jsonrpcReservedErrorRangeEnd */ const serverErrorEnd: -32000 } export interface ResponseErrorLiteral { /** * A number indicating the error type that occurred. */ code: number /** * A string providing a short description of the error. */ message: string /** * A Primitive or Structured value that contains additional * information about the error. Can be omitted. */ data?: D } /** * An error object return in a response in case a request * has failed. */ export class ResponseError extends Error { readonly code: number readonly data: D | undefined constructor(code: number, message: string, data?: D) toJson(): ResponseErrorLiteral } /** * A language server message */ export interface Message { jsonrpc: string } export interface AbstractCancellationTokenSource extends Disposable { token: CancellationToken cancel(): void } /** * A response message. */ export interface ResponseMessage extends Message { /** * The request id. */ id: number | string | null /** * The result of a request. This member is REQUIRED on success. * This member MUST NOT exist if there was an error invoking the method. */ result?: string | number | boolean | object | any[] | null /** * The error object in case a request fails. */ error?: ResponseErrorLiteral } // }} // nvim interfaces {{ type VimValue = | number | boolean | string | number[] | { [key: string]: any } // see `:h nvim_set_client_info()` for details. export interface VimClientInfo { name: string version: { major?: number minor?: number patch?: number prerelease?: string commit?: string } type: 'remote' | 'embedder' | 'host' methods?: { [index: string]: any } attributes?: { [index: string]: any } } export interface UiAttachOptions { rgb?: boolean ext_popupmenu?: boolean ext_tabline?: boolean ext_wildmenu?: boolean ext_cmdline?: boolean ext_linegrid?: boolean ext_hlstate?: boolean } export interface ChanInfo { id: number stream: 'stdio' | 'stderr' | 'socket' | 'job' mode: 'bytes' | 'terminal' | 'rpc' pty?: number buffer?: number client?: VimClientInfo } /** * Returned by nvim_get_commands api. */ export interface VimCommandDescription { name: string bang: boolean bar: boolean register: boolean definition: string count?: number | null script_id: number complete?: string nargs?: string range?: string complete_arg?: string } export interface NvimFloatOptions { standalone?: boolean focusable?: boolean relative?: 'editor' | 'cursor' | 'win' | 'mouse' anchor?: 'NW' | 'NE' | 'SW' | 'SE' border?: 'none' | 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | string[] style?: 'minimal' title?: string title_pos?: 'left' | 'center' | 'right' footer?: string | [string, string][] footer_pos?: 'left' | 'center' | 'right' noautocmd?: boolean fixed?: boolean hide?: boolean height: number width: number row: number col: number } export interface ExtmarkOptions { id?: number // 0-based inclusive. end_line?: number // 0-based exclusive. end_col?: number // name of the highlight group used to highlight this mark. hl_group?: string hl_mode?: 'replace' | 'combine' | 'blend' hl_eol?: boolean // A list of [text, highlight] tuples virt_text?: [string, string | string[]][] virt_text_pos?: 'eol' | 'overlay' | 'right_align' | 'inline' virt_text_win_col?: number virt_text_hide?: boolean virt_lines?: [string, string | string[]][][] virt_lines_above?: boolean virt_lines_leftcol?: boolean right_gravity?: boolean end_right_gravity?: boolean priority?: number } export interface ExtmarkDetails { end_col: number end_row: number priority: number hl_group?: string virt_text?: [string, string][] virt_lines?: [string, string | string][][] } export interface NvimProc { ppid: number name: string pid: number } export interface SignPlaceOption { id?: number group?: string name: string lnum: number priority?: number } export interface SignUnplaceOption { group?: string id?: number } export interface SignPlacedOption { /** * Use '*' for all group, default to '' as unnamed group. */ group?: string id?: number lnum?: number } export interface SignItem { group: string id: number lnum: number name: string priority: number } export interface HighlightItem { hlGroup: string /** * 0 based */ lnum: number /** * 0 based */ colStart: number /** * 0 based */ colEnd: number } export interface ExtendedHighlightItem extends HighlightItem { combine?: boolean start_incl?: boolean end_incl?: boolean } export interface HighlightOption { /** * 0 based start line, default to 0. */ start?: number /** * 0 based end line, default to 0. */ end?: number /** * Default to 0 on vim8, 4096 on neovim */ priority?: number /** * Buffer changedtick to match. */ changedtick?: number } /** * All values default to `false`, see `:h :map-arguments` */ export interface BufferKeymapOption { desc?: string noremap?: boolean nowait?: boolean silent?: boolean script?: boolean expr?: boolean unique?: boolean // vim9 only special?: boolean } export interface BufferHighlight { /** * Name of the highlight group to use */ hlGroup?: string /** * Namespace to use or -1 for ungrouped highlight */ srcId?: number /** * Line to highlight (zero-indexed) */ line?: number /** * Start of (byte-indexed) column range to highlight */ colStart?: number /** * End of (byte-indexed) column range to highlight, or -1 to highlight to end of line */ colEnd?: number } export interface BufferClearHighlight { srcId?: number lineStart?: number lineEnd?: number } export interface VirtualTextOption { /** * Used on vim9 and neovim >= 0.10.0. */ col?: number /** * Add line indent when text_align is below or above. */ indent?: boolean /** * highlight mode, blend is neovim only (replace is used on vim when specified). */ hl_mode?: 'combine' | 'replace' | 'blend' /** * neovim and vim. */ text_align?: 'after' | 'right' | 'below' | 'above' /** * neovim only, right_gravity of nvim_buf_set_extmark. */ right_gravity?: boolean /** * neovim only */ virt_text_win_col?: number /** * vim9 only */ text_wrap?: 'wrap' | 'truncate' } export interface AugroupOption { /** * Clear the all autocmds before create autocmd group, default to `true`. */ clear?: boolean } interface AutocmdOption { /** * Group name or group id from `nvim.createAugroup()`, see `:h autocmd-groups`. */ group?: string | number /** * Pattern to match, see `:h autocmd-pattern`. */ pattern?: string | string[] /** * Buffer number for buflocal autocommand, see `:h autocmd-buflocal`. */ buffer?: number /** * Description test, not used on vim9. */ desc?: string /** * Vim command to run when trigger autocommand. */ command?: string /** * See `:h autocmd-once`. */ once?: boolean /** * See `:h autocmd-nested`. */ nested?: boolean /** * Vim9 only, see `:h autocmd_add()` */ replace?: boolean } interface BaseApi { /** * unique identify number */ id: number /** * Check if same by compare id. */ equals(other: T): boolean /** * Request to vim, name need to be nvim_ prefixed and supported by vim. * * @param {string} name - nvim function name * @param {VimValue[]} args * @returns {Promise} */ request(name: string, args?: VimValue[]): Promise /** * Send notification to vim, name need to be nvim_ prefixed and supported * by vim */ notify(name: string, args?: VimValue[]): void /** * Retrieves scoped variable, returns null when value doesn't exist. */ getVar(name: string): Promise /** * Set scoped variable by request. * * @param {string} name * @param {VimValue} value * @returns {Promise} */ setVar(name: string, value: VimValue): Promise /** * Set scoped variable by notification. */ setVar(name: string, value: VimValue, isNotify: true): void /** * Delete scoped variable by notification. */ deleteVar(name: string): void /** * Retrieves a scoped option, doesn't exist for tabpage. * * Note: neovim returns true/false for boolean option, but it would be 0/1 * on vim. */ getOption(name: string): Promise /** * Set scoped option by request, doesn't exist for tabpage. */ setOption(name: string, value: VimValue): Promise /** * Set scoped variable by notification, doesn't exist for tabpage. */ setOption(name: string, value: VimValue, isNotify: true): void } export const nvim: Neovim export interface Neovim extends BaseApi { /** * Echo error message to vim and log error stack. */ echoError(error: Error | string): void /** * Check if `nvim_` function exists. * * @deprecated use `workspace.has` to check vim version instead. */ hasFunction(name: string): boolean /** * Get channelid used by coc.nvim. */ channelId: Promise /** * Create buffer instance by id. */ createBuffer(id: number): Buffer /** * Create window instance by id. */ createWindow(id: number): Window /** * Create tabpage instance by id. */ createTabpage(id: number): Tabpage /** * Stop send subsequent notifications. * This method **must** be paired with `nvim.resumeNotification` in a sync manner. */ pauseNotification(): void /** * Send paused notifications by nvim_call_atomic request * * @param {boolean} redrawVim Redraw vim on vim8 to update the screen * * **Note**: avoid call async function between pauseNotification and * resumeNotification. */ resumeNotification(redrawVim?: boolean): Promise<[VimValue[], [string, number, string] | null]> /** * Send paused notifications by nvim_call_atomic notification * * @param {boolean} redrawVim Redraw vim to update the screen * @param {true} notify * @returns {void} */ resumeNotification(redrawVim: boolean, notify: true): void /** * Send redraw command to vim, does nothing on neovim since it's not necessary on most cases. */ redrawVim(): void /** * Get list of current buffers. */ buffers: Promise /** * Get current buffer. */ buffer: Promise /** * Set current buffer */ setBuffer(buffer: Buffer): Promise /** * Get list of current tabpages. */ tabpages: Promise /** * Get current tabpage. */ tabpage: Promise /** * Set current tabpage */ setTabpage(tabpage: Tabpage): Promise /** * Get list of current windows. */ windows: Promise /** * Get current window. */ window: Promise /** * Set current window. */ setWindow(window: Window): Promise /** * Get information of all channels, * **Note:** works on neovim only. */ chans: Promise /** * Get information of channel by id, * **Note:** works on neovim only. */ getChanInfo(id: number): Promise /** * Creates a new namespace, or gets an existing one. * `:h nvim_create_namespace()` */ createNamespace(name?: string): Promise /** * Gets existing, non-anonymous namespaces. * **Note:** works on neovim only. * * @return dict that maps from names to namespace ids. */ namespaces: Promise<{ [name: string]: number }> /** * Gets a map of global (non-buffer-local) Ex commands. * * @return Map of maps describing commands. */ getCommands(opt?: { builtin: boolean }): Promise<{ [name: string]: VimCommandDescription }> /** * Get list of all runtime paths */ runtimePaths: Promise /** * Set global working directory. * **Note:** works on neovim only. */ setDirectory(dir: string): Promise /** * Get current line. */ line: Promise /** * Creates a new, empty, unnamed buffer. */ createNewBuffer(listed?: boolean, scratch?: boolean): Promise /** * Create float window of neovim. * * **Note:** works on neovim only, use high level api provided by window * module is recommended. */ openFloatWindow(buffer: Buffer, enter: boolean, options: NvimFloatOptions): Promise /** * Set current line. */ setLine(line: string): Promise /** * Gets a list of global (non-buffer-local) |mapping| definitions. * `:h nvim_get_keymap` * * **Note:** works on neovim only. */ getKeymap(mode: string): Promise /** * Gets the current mode. |mode()| "blocking" is true if Nvim is waiting for input. * * **Note:** blocking would always be false when used with vim. */ mode: Promise<{ mode: string; blocking: boolean }> /** * Returns a map of color names and RGB values. * * **Note:** works on neovim only. */ colorMap(): Promise<{ [name: string]: number }> /** * Returns the 24-bit RGB value of a |nvim_get_color_map()| color name or * "#rrggbb" hexadecimal string. * * **Note:** works on neovim only. */ getColorByName(name: string): Promise /** * Gets a highlight definition by id. |hlID()| * * **Note:** works on neovim only. */ getHighlight(nameOrId: string | number, isRgb?: boolean): Promise /** * Get a highlight by name, return rgb by default. * * **Note:** works on neovim only. */ getHighlightByName(name: string, isRgb?: boolean): Promise /** * Get a highlight by id, return rgb by default. * * **Note:** works on neovim only. */ getHighlightById(id: number, isRgb?: boolean): Promise /** * Delete current line in buffer. */ deleteCurrentLine(): Promise /** * Evaluates a VimL expression (:help expression). Dictionaries * and Lists are recursively expanded. On VimL error: Returns a * generic error; v:errmsg is not updated. * */ eval(expr: string): Promise /** * Executes lua, it's possible neovim client does not support this * * **Note:** works on neovim only. */ lua(code: string, args?: VimValue[]): Promise /** * Calls a VimL |Dictionary-function| with the given arguments. */ callDictFunction(dict: object | string, fname: string, args: VimValue | VimValue[]): Promise /** * Call a vim function. * * @param {string} fname - function name * @param {VimValue | VimValue[]} args * @returns {Promise} */ call(fname: string, args?: VimValue | VimValue[]): Promise /** * Call a vim function by notification. */ call(fname: string, args: VimValue | VimValue[], isNotify: true): void /** * Use call command `:h channel-commands` to call function on vim9. * Warning: NodeJS side only get the 'ERROR' text on error, to get error message, * see `:h coc-api-channel` */ callVim(fname: string, args?: VimValue | VimValue[]): Promise /** * Use call command `:h channel-commands` to call function on vim9. * Warning: errors not exists on NodeJS side, see `:h coc-api-channel` */ callVim(fname: string, args: VimValue | VimValue[], isNotify: true): void /** * Use expr command `:h channel-commands` to eval expression on vim9. * Warning: NodeJS side only get the 'ERROR' text on error, to get error message, * see `:h coc-api-channel` */ evalVim(expr: string): Promise /** * Use ex command `:h channel-commands` to execute command on vim9. * Warning: errors not exists on NodeJS side, see `:h coc-api-channel` */ exVim(command: string): void /** * Call a vim function with timer of timeout 0. * * @param {string} fname - function name * @param {VimValue | VimValue[]} args * @returns {Promise} */ callTimer(fname: string, args?: VimValue | VimValue[]): Promise /** * Call a vim function with timer of timeout 0 by notification. */ callTimer(fname: string, args: VimValue | VimValue[], isNotify: true): void /** * Call async vim function that accept callback as argument * by using notifications. */ callAsync(fname: string, args?: VimValue | VimValue[]): Promise /** * Calls many API methods atomically. */ callAtomic(calls: [string, VimValue[]][]): Promise<[any[], any[] | null]> /** * Executes an ex-command by request. */ command(arg: string): Promise /** * Executes an ex-command by notification. */ command(arg: string, isNotify: true): void /** * Runs a command and returns output. * * @deprecated Use exec() instead. */ commandOutput(arg: string): Promise /** * Executes Vimscript (multiline block of Ex-commands), like anonymous |:source| */ exec(src: string, output?: boolean): Promise /** * Gets a v: variable. */ getVvar(name: string): Promise /** * `:h nvim_feedkeys` */ feedKeys(keys: string, mode: string, escapeCsi: boolean): Promise /** * Add global keymap by notification, `:h nvim_set_keymap` */ setKeymap(mode: string, lhs: string, rhs: string, opts?: BufferKeymapOption): void /** * Delete global keymap, `:h nvim_del_keymap` */ deleteKeymap(mode: string, lhs: string): void /** * Queues raw user-input. Unlike |nvim_feedkeys()|, this uses a * low-level input buffer and the call is non-blocking (input is * processed asynchronously by the eventloop). * * On execution error: does not fail, but updates v:errmsg. * * **Note:** works on neovim only. */ input(keys: string): Promise /** * Parse a VimL Expression. */ parseExpression(expr: string, flags: string, highlight: boolean): Promise /** * Get process info, neovim only. * * **Note:** works on neovim only. */ getProc(pid: number): Promise /** * Gets the immediate children of process `pid`. * * **Note:** works on neovim only. */ getProcChildren(pid: number): Promise /** * Replaces terminal codes and |keycodes| (, , ...) * in a string with the internal representation. * * **Note:** works on neovim only. */ replaceTermcodes(str: string, fromPart: boolean, doIt: boolean, special: boolean): Promise /** * Gets width(display cells) of string. */ strWidth(str: string): Promise /** * Create autocmd group with {name} and {option} */ createAugroup(name: string, option?: AugroupOption): Promise /** * Create autocmd group with {name} and {option}, use notification to vim. */ createAugroup(name: string, option: AugroupOption, isNotify: true): void /** * Create autocmd with {event} and {option} */ createAutocmd(event: string | string[], option?: AutocmdOption): Promise /** * Create autocmd with {event} and {option} */ createAutocmd(event: string | string[], option: AutocmdOption, isNotify: true): void /** * Delete autocmd with {id} returned from `nvim.createAutocmd()` * Notice: vim9 can't support delete specific autocmd yet, autocmds which * have the same `group` `event` `pattern` are all cleared. */ deleteAutocmd(id: number): void /** * Gets a list of dictionaries representing attached UIs. * * **Note:** works on neovim only. */ uis: Promise /** * Subscribe to nvim event broadcasts. * * **Note:** works on neovim only. */ subscribe(event: string): Promise /** * Unsubscribe to nvim event broadcasts * * **Note:** works on neovim only. */ unsubscribe(event: string): Promise /** * Quit vim. */ quit(): Promise } export interface Buffer extends BaseApi { id: number /** Total number of lines in buffer */ length: Promise /** * Get lines of buffer. */ lines: Promise /** * Get changedtick of buffer. */ changedtick: Promise /** * Add buffer keymap by notification, `:h nvim_buf_set_keymap` */ setKeymap(mode: string, lhs: string, rhs: string, opts?: BufferKeymapOption): void /** * Delete buffer keymap, `:h nvim_buf_del_keymap` */ deleteKeymap(mode: string, lhs: string): void /** * Removes an ext mark by notification. Neovim only. * * @public * @param {number} ns_id - Namespace id * @param {number} id - Extmark id */ deleteExtMark(ns_id: number, id: number): void /** * Gets the position (0-indexed) of an extmark. Neovim only. * * @param {number} ns_id - Namespace id * @param {number} id - Extmark id * @param {Object} opts - Optional parameters. * @returns {Promise<[] | [number, number] | [number, number, ExtmarkDetails]>} */ getExtMarkById(ns_id: number, id: number, opts?: { details?: boolean }): Promise<[] | [number, number] | [number, number, ExtmarkDetails]> /** * Gets extmarks in "traversal order" from a |charwise| region defined by * buffer positions (inclusive, 0-indexed |api-indexing|). * * Region can be given as (row,col) tuples, or valid extmark ids (whose * positions define the bounds). 0 and -1 are understood as (0,0) and (-1,-1) * respectively, thus the following are equivalent: * * nvim_buf_get_extmarks(0, my_ns, 0, -1, {}) * nvim_buf_get_extmarks(0, my_ns, [0,0], [-1,-1], {}) * * @param {number} ns_id - Namespace id * @param {[number, number] | number} start * @param {[number, number] | number} end * @param {Object} opts * @returns {Promise<[number, number, number, ExtmarkDetails?][]>} */ getExtMarks(ns_id: number, start: [number, number] | number, end: [number, number] | number, opts?: { details?: boolean limit?: number }): Promise<[number, number, number, ExtmarkDetails?][]> /** * Creates or updates an extmark by notification, `:h nvim_buf_set_extmark`. * * @param {number} ns_id * @param {number} line * @param {number} col * @param {ExtmarkOptions} opts * @returns {void} */ setExtMark(ns_id: number, line: number, col: number, opts?: ExtmarkOptions): void /** * Add sign to buffer by notification. * * @param {SignPlaceOption} sign */ placeSign(sign: SignPlaceOption): void /** * Unplace signs by notification */ unplaceSign(opts: SignUnplaceOption): void /** * Get signs by group name or id and lnum. * * @param {SignPlacedOption} opts */ getSigns(opts: SignPlacedOption): Promise /** * Get highlight items by namespace (end inclusive). * * @param {string} ns Namespace key or id. * @param {number} start 0 based line number, default to 0. * @param {number} end 0 based line number, default to -1. */ getHighlights(ns: string, start?: number, end?: number): Promise /** * Update namespaced highlights in range by notification. * Priority default to 0 on vim and 4096 on neovim. * Note: timer used for whole buffer highlights for better performance. * * @param {string} ns Namespace key. * @param {HighlightItem[]} highlights Highlight items. * @param {HighlightOption} opts Highlight options. */ updateHighlights(ns: string, highlights: ExtendedHighlightItem[], opts?: HighlightOption): void /** * Gets a map of buffer-local |user-commands|. * * **Note:** works on neovim only. */ getCommands(options?: {}): Promise /** * Get lines of buffer, get all lines by default. */ getLines(opts?: { start: number, end: number, strictIndexing?: boolean }): Promise /** * Set lines of buffer given indices use request. */ setLines(lines: string[], opts?: { start: number, end: number, strictIndexing?: boolean }): Promise /** * Set lines of buffer given indices use notification. */ setLines(lines: string[], opts: { start: number, end: number, strictIndexing?: boolean }, isNotify: true): void /** * Set virtual text for a line use notification, works on both neovim and vim9. * * @public * @param {number} src_id - Source group to use or 0 to use a new group, or -1 * @param {number} line - Line to annotate with virtual text (zero-indexed) * @param {Chunk[]} chunks - List with [text, hl_group] * @param {[index} opts * @returns {Promise} */ setVirtualText(src_id: number, line: number, chunks: [string, string][], opts?: VirtualTextOption): void /** * Append a string or list of lines to end of buffer */ append(lines: string[] | string): Promise /** * Get buffer name. */ name: Promise /** * Set buffer name. */ setName(name: string): Promise /** * Check if buffer valid. */ valid: Promise /** * Get mark position given mark name * * **Note:** works on neovim only. */ mark(name: string): Promise<[number, number]> /** * Gets a list of buffer-local |mapping| definitions. * * @return Array of maparg()-like dictionaries describing mappings. * The "buffer" key holds the associated buffer handle. */ getKeymap(mode: string): Promise /** * Check if buffer loaded. */ loaded: Promise /** * Returns the byte offset for a line. * * Line 1 (index=0) has offset 0. UTF-8 bytes are counted. EOL is * one byte. 'fileformat' and 'fileencoding' are ignored. The * line index just after the last line gives the total byte-count * of the buffer. A final EOL byte is counted if it would be * written, see 'eol'. * * Unlike |line2byte()|, throws error for out-of-bounds indexing. * Returns -1 for unloaded buffer. * * @return {Number} Integer byte offset, or -1 for unloaded buffer. */ getOffset(index: number): Promise /** * Adds a highlight to buffer, checkout |nvim_buf_add_highlight|. * * Note: when `srcId = 0`, request is made for new `srcId`, otherwire, use notification. * Note: `hlGroup` as empty string is not supported. * * @deprecated use `highlightRanges()` instead. */ addHighlight(opts: BufferHighlight): Promise /** * Clear highlights of specified lines. * * @deprecated use clearNamespace() instead. */ clearHighlight(args?: BufferClearHighlight) /** * Add highlight to ranges by notification, works on both vim & neovim. * * Works on neovim and `workspace.isVim && workspace.env.textprop` is true * * @param {string | number} srcId Unique key or namespace number. * @param {string} hlGroup Highlight group. * @param {Range[]} ranges List of highlight ranges */ highlightRanges(srcId: string | number, hlGroup: string, ranges: Range[]): void /** * Clear namespace by id or name by notification, works on both vim & neovim. * * Works on neovim and `workspace.isVim && workspace.env.textprop` is true * * @param key Unique key or namespace number, use -1 for all namespaces * @param lineStart Start of line, 0 based, default to 0. * @param lineEnd End of line, 0 based, default to -1. */ clearNamespace(key: number | string, lineStart?: number, lineEnd?: number) } export interface Window extends BaseApi { /** * The windowid that not change within a Vim session */ id: number /** * Buffer in window. */ buffer: Promise /** * Tabpage contains window. */ tabpage: Promise /** * Cursor position as [line, col], 1 based. */ cursor: Promise<[number, number]> /** * Window height. */ height: Promise /** * Window width. */ width: Promise /** * Set cursor position by request. */ setCursor(pos: [number, number]): Promise /** * Set cursor position by notification. */ setCursor(pos: [number, number], isNotify: true): void /** * Set height */ setHeight(height: number): Promise /** * Set height by notification. */ setHeight(height: number, isNotify: true): void /** * Set width. */ setWidth(width: number): Promise /** * Set width by notification. */ setWidth(width: number, isNotify: true): void /** * Get window position, not work with vim8's popup. */ position: Promise<[number, number]> /** 0-indexed, on-screen window position(row) in display cells. */ row: Promise /** 0-indexed, on-screen window position(col) in display cells. */ col: Promise /** * Check if window valid. */ valid: Promise /** * Get window number, throws for invalid window. */ number: Promise /** * Config float window with options. * * **Note:** works on neovim only. */ setConfig(options: NvimFloatOptions): Promise /** * Config float window with options by send notification. * * **Note:** works on neovim only. */ setConfig(options: NvimFloatOptions, isNotify: true): void /** * Gets window configuration. * * **Note:** works on neovim only. * * @returns Map defining the window configuration, see |nvim_open_win()| */ getConfig(): Promise /** * Close window by send request. */ close(force: boolean): Promise /** * Close window by send notification. */ close(force: boolean, isNotify: true): void /** * Add highlight to ranges by request (matchaddpos is used) * * @return {Promise} match ids. */ highlightRanges(hlGroup: string, ranges: Range[], priority?: number): Promise /** * Add highlight to ranges by notification (matchaddpos is used) */ highlightRanges(hlGroup: string, ranges: Range[], priority: number, isNotify: true): void /** * Clear match of highlight group by send notification. */ clearMatchGroup(hlGroup: string): void /** * Clear match of match ids by send notification. */ clearMatches(ids: number[]): void } export interface Tabpage extends BaseApi { /** * tabpage number. */ number: Promise /** * Is current tabpage valid. */ valid: Promise /** * Returns all windows of tabpage. */ windows: Promise /** * Current window of tabpage. */ window: Promise } // }} // vscode-uri {{ export interface UriComponents { scheme: string authority: string path: string query: string fragment: string } /** * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. * This class is a simple parser which creates the basic component parts * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation * and encoding. * * ```txt * foo://example.com:8042/over/there?name=ferret#nose * \_/ \______________/\_________/ \_________/ \__/ * | | | | | * scheme authority path query fragment * | _____________________|__ * / \ / \ * urn:example:animal:ferret:nose * ``` */ export class Uri implements UriComponents { static isUri(thing: any): thing is Uri /** * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'. * The part before the first colon. */ readonly scheme: string /** * authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'. * The part between the first double slashes and the next slash. */ readonly authority: string /** * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. */ readonly path: string /** * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. */ readonly query: string /** * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. */ readonly fragment: string /** * @internal */ protected constructor(scheme: string, authority?: string, path?: string, query?: string, fragment?: string, _strict?: boolean) /** * @internal */ protected constructor(components: UriComponents) /** * Returns a string representing the corresponding file system path of this URI. * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the * platform specific path separator. * * * Will *not* validate the path for invalid characters and semantics. * * Will *not* look at the scheme of this URI. * * The result shall *not* be used for display purposes but for accessing a file on disk. * * * The *difference* to `URI#path` is the use of the platform specific separator and the handling * of UNC paths. See the below sample of a file-uri with an authority (UNC path). * * ```ts const u = URI.parse('file://server/c$/folder/file.txt') u.authority === 'server' u.path === '/shares/c$/file.txt' u.fsPath === '\\server\c$\folder\file.txt' ``` * * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path, * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working * with URIs that represent files on disk (`file` scheme). */ readonly fsPath: string with(change: { scheme?: string authority?: string | null path?: string | null query?: string | null fragment?: string | null }): Uri /** * Creates a new URI from a string, e.g. `http://www.msft.com/some/path`, * `file:///usr/home`, or `scheme:with/path`. * * @param value A string which represents an URI (see `URI#toString`). */ static parse(value: string, _strict?: boolean): Uri /** * Creates a new URI from a file system path, e.g. `c:\my\files`, * `/usr/home`, or `\\server\share\some\path`. * * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as** * `URI.parse('file://' + path)` because the path might contain characters that are * interpreted (# and ?). See the following sample: * ```ts const good = URI.file('/coding/c#/project1'); good.scheme === 'file'; good.path === '/coding/c#/project1'; good.fragment === ''; const bad = URI.parse('file://' + '/coding/c#/project1'); bad.scheme === 'file'; bad.path === '/coding/c'; // path is now broken bad.fragment === '/project1'; ``` * * @param path A file system path (see `URI#fsPath`) */ static file(path: string): Uri static from(components: { scheme: string authority?: string path?: string query?: string fragment?: string }): Uri /** * Creates a string representation for this URI. It's guaranteed that calling * `URI.parse` with the result of this function creates an URI which is equal * to this URI. * * * The result shall *not* be used for display purposes but for externalization or transport. * * The result will be encoded using the percentage encoding and encoding happens mostly * ignore the scheme-specific encoding rules. * * @param skipEncoding Do not encode the result, default is `false` */ toString(skipEncoding?: boolean): string toJSON(): UriComponents } // }} // vim interfaces {{ /** * See `:h complete-items` */ export interface VimCompleteItem { word: string abbr?: string menu?: string /** * @deprecated use documentation property. */ info?: string kind?: string icase?: number equal?: number dup?: number empty?: number user_data?: string /** * The same as deprecated tag. */ deprecated?: boolean /** * Additional details for a completion item label. */ labelDetails?: CompletionItemLabelDetails /** * A string that should be used when comparing this item * with other items. When `falsy` the [word](#VimCompleteItem.word) * is used. */ sortText?: string /** * A string that should be used when filtering a set of * completion items. When `falsy` the [word](#VimCompleteItem.word) * is used. */ filterText?: string /** * Text to insert, could be snippet text. */ insertText?: string /** * When `true` and onCompleteDone handler not exists on source, the snippet * would be expanded after confirm completion. */ isSnippet?: boolean /** * Docs to shown in detail window. */ documentation?: Documentation[] } export interface CompleteDoneItem { readonly word: string readonly abbr?: string readonly source: string readonly isSnippet: boolean readonly kind?: string | CompletionItemKind readonly menu?: string } export interface LocationListItem { bufnr: number lnum: number end_lnum: number col: number end_col: number text: string type: string } export interface QuickfixItem { uri?: string module?: string range?: Range text?: string type?: string filename?: string bufnr?: number lnum?: number end_lnum?: number col?: number end_col?: number valid?: boolean nr?: number } // }} // provider interfaces {{ /** * A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider), * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a * thenable. * * The snippets below are all valid implementations of the [`HoverProvider`](#HoverProvider): * * ```ts * let a: HoverProvider = { * provideHover(doc, pos, token): ProviderResult { * return new Hover('Hello World') * } * } * * let b: HoverProvider = { * provideHover(doc, pos, token): ProviderResult { * return new Promise(resolve => { * resolve(new Hover('Hello World')) * }) * } * } * * let c: HoverProvider = { * provideHover(doc, pos, token): ProviderResult { * return; // undefined * } * } * ``` */ export type ProviderResult = | T | undefined | null | Thenable /** * Supported provider names. */ export enum ProviderName { FormatOnType = 'formatOnType', Rename = 'rename', OnTypeEdit = 'onTypeEdit', DocumentLink = 'documentLink', DocumentColor = 'documentColor', FoldingRange = 'foldingRange', Format = 'format', CodeAction = 'codeAction', FormatRange = 'formatRange', Hover = 'hover', Signature = 'signature', WorkspaceSymbols = 'workspaceSymbols', DocumentSymbol = 'documentSymbol', DocumentHighlight = 'documentHighlight', Definition = 'definition', Declaration = 'declaration', TypeDefinition = 'typeDefinition', Reference = 'reference', Implementation = 'implementation', CodeLens = 'codeLens', SelectionRange = 'selectionRange', CallHierarchy = 'callHierarchy', SemanticTokens = 'semanticTokens', SemanticTokensRange = 'semanticTokensRange', LinkedEditing = 'linkedEditing', InlayHint = 'inlayHint', InlineValue = 'inlineValue', InlineCompletion = 'inlineCompletion', TypeHierarchy = 'typeHierarchy' } /** * The completion item provider interface defines the contract between extensions and * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). * * Providers can delay the computation of the [`detail`](#CompletionItem.detail) * and [`documentation`](#CompletionItem.documentation) properties by implementing the * [`resolveCompletionItem`](#CompletionItemProvider.resolveCompletionItem)-function. However, properties that * are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `range`, must * not be changed during resolve. * * Providers are asked for completions either explicitly by a user gesture or -depending on the configuration- * implicitly when typing words or trigger characters. */ export interface CompletionItemProvider { /** * Provide completion items for the given position and document. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @param context How the completion was triggered. * * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ provideCompletionItems( document: LinesTextDocument, position: Position, token: CancellationToken, context?: CompletionContext ): ProviderResult /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) * or [details](#CompletionItem.detail). * * The editor will only resolve a completion item once. * * @param item A completion item currently active in the UI. * @param token A cancellation token. * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ resolveCompletionItem?( item: CompletionItem, token: CancellationToken ): ProviderResult } /** * The hover provider interface defines the contract between extensions and * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ export interface HoverProvider { /** * Provide a hover for the given position and document. Multiple hovers at the same * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A hover or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideHover( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) * and peek definition features. */ export interface DefinitionProvider { /** * Provide the definition of the symbol at the given position and document. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideDefinition( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) * and peek definition features. */ export interface DeclarationProvider { /** * Provide the declaration of the symbol at the given position and document. */ provideDeclaration( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The signature help provider interface defines the contract between extensions and * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ export interface SignatureHelpProvider { /** * Provide help for the signature at the given position and document. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return Signature help or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideSignatureHelp( document: LinesTextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext ): ProviderResult } /** * The type definition provider defines the contract between extensions and * the go to type definition feature. */ export interface TypeDefinitionProvider { /** * Provide the type definition of the symbol at the given position and document. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideTypeDefinition( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The reference provider interface defines the contract between extensions and * the [find references](https://code.visualstudio.com/docs/editor/editingevolved#_peek)-feature. */ export interface ReferenceProvider { /** * Provide a set of project-wide references for the given position and document. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param context * @param token A cancellation token. * @return An array of locations or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideReferences( document: LinesTextDocument, position: Position, context: ReferenceContext, token: CancellationToken ): ProviderResult } /** * Folding context (for future use) */ export interface FoldingContext {} /** * The folding range provider interface defines the contract between extensions and * [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding) in the editor. */ export interface FoldingRangeProvider { /** * An optional event to signal that the folding ranges from this provider have changed. */ onDidChangeFoldingRanges?: Event /** * Returns a list of folding ranges or null and undefined if the provider * does not want to participate or was cancelled. * * @param document The document in which the command was invoked. * @param context Additional context information (for future use) * @param token A cancellation token. */ provideFoldingRanges( document: LinesTextDocument, context: FoldingContext, token: CancellationToken ): ProviderResult } /** * The document symbol provider interface defines the contract between extensions and * the [go to symbol](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-symbol)-feature. */ export interface DocumentSymbolProvider { /** * Provide symbol information for the given document. * * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentSymbols( document: LinesTextDocument, token: CancellationToken ): ProviderResult } /** * The implementation provider interface defines the contract between extensions and * the go to implementation feature. */ export interface ImplementationProvider { /** * Provide the implementations of the symbol at the given position and document. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A definition or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideImplementation( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The workspace symbol provider interface defines the contract between extensions and * the [symbol search](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name)-feature. */ export interface WorkspaceSymbolProvider { /** * Project-wide search for a symbol matching the given query string. It is up to the provider * how to search given the query string, like substring, indexOf etc. To improve performance implementors can * skip the [location](#WorkspaceSymbol.location) of symbols and implement `resolveWorkspaceSymbol` to do that * later. * * The `query`-parameter should be interpreted in a *relaxed way* as the editor will apply its own highlighting * and scoring on the results. A good rule of thumb is to match case-insensitive and to simply check that the * characters of *query* appear in their order in a candidate symbol. Don't use prefix, substring, or similar * strict matching. * * @param query A non-empty query string. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideWorkspaceSymbols( query: string, token: CancellationToken ): ProviderResult /** * Given a symbol fill in its [location](#WorkspaceSymbol.location). This method is called whenever a symbol * is selected in the UI. Providers can implement this method and return incomplete symbols from * [`provideWorkspaceSymbols`](#WorkspaceSymbolProvider.provideWorkspaceSymbols) which often helps to improve * performance. * * @param symbol The symbol that is to be resolved. Guaranteed to be an instance of an object returned from an * earlier call to `provideWorkspaceSymbols`. * @param token A cancellation token. * @return The resolved symbol or a thenable that resolves to that. When no result is returned, * the given `symbol` is used. */ resolveWorkspaceSymbol?( symbol: WorkspaceSymbol, token: CancellationToken ): ProviderResult } /** * The rename provider interface defines the contract between extensions and * the [rename](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol)-feature. */ export interface RenameProvider { /** * Provide an edit that describes changes that have to be made to one * or many resources to rename a symbol to a different name. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param newName The new name of the symbol. If the given name is not valid, the provider must return a rejected promise. * @param token A cancellation token. * @return A workspace edit or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideRenameEdits( document: LinesTextDocument, position: Position, newName: string, token: CancellationToken ): ProviderResult /** * Optional function for resolving and validating a position *before* running rename. The result can * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol * which is being renamed - when omitted the text in the returned range is used. * * @param document The document in which rename will be invoked. * @param position The position at which rename will be invoked. * @param token A cancellation token. * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. */ prepareRename?( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ export interface DocumentFormattingEditProvider { /** * Provide formatting edits for a whole document. * * @param document The document in which the command was invoked. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentFormattingEdits( document: LinesTextDocument, options: FormattingOptions, token: CancellationToken ): ProviderResult } /** * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ export interface DocumentRangeFormattingEditProvider { /** * Provide formatting edits for a range in a document. * * The given range is a hint and providers can decide to format a smaller * or larger range. Often this is done by adjusting the start and end * of the range to full syntax nodes. * * @param document The document in which the command was invoked. * @param range The range which should be formatted. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentRangeFormattingEdits( document: LinesTextDocument, range: Range, options: FormattingOptions, token: CancellationToken ): ProviderResult } /** * The code action interface defines the contract between extensions and * the [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. * * A code action can be any command that is [known](#commands.getCommands) to the system. */ export interface CodeActionProvider { /** * Provide commands for the given document and range. * * @param document The document in which the command was invoked. * @param range The selector or range for which the command was invoked. This will always be a selection if * there is a currently active editor. * @param context Context carrying additional information. * @param token A cancellation token. * @return An array of commands, quick fixes, or refactorings or a thenable of such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideCodeActions( document: LinesTextDocument, range: Range, context: CodeActionContext, token: CancellationToken ): ProviderResult<(Command | CodeAction)[]> /** * Given a code action fill in its [`edit`](#CodeAction.edit)-property. Changes to * all other properties, like title, are ignored. A code action that has an edit * will not be resolved. * * @param codeAction A code action. * @param token A cancellation token. * @return The resolved code action or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ resolveCodeAction?(codeAction: T, token: CancellationToken): ProviderResult } /** * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) providers */ export interface CodeActionProviderMetadata { /** * [CodeActionKinds](#CodeActionKind) that this provider may return. * * The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the provider * may list our every specific kind they provide, such as `CodeActionKind.Refactor.Extract.append('function`)` */ readonly providedCodeActionKinds?: ReadonlyArray } /** * The document highlight provider interface defines the contract between extensions and * the word-highlight-feature. */ export interface DocumentHighlightProvider { /** * Provide a set of document highlights, like all occurrences of a variable or * all exit-points of a function. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentHighlights( document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } /** * The document link provider defines the contract between extensions and feature of showing * links in the editor. */ export interface DocumentLinkProvider { /** * Provide links for the given document. Note that the editor ships with a default provider that detects * `http(s)` and `file` links. * * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentLinks(document: LinesTextDocument, token: CancellationToken): ProviderResult /** * Given a link fill in its [target](#DocumentLink.target). This method is called when an incomplete * link is selected in the UI. Providers can implement this method and return incomple links * (without target) from the [`provideDocumentLinks`](#DocumentLinkProvider.provideDocumentLinks) method which * often helps to improve performance. * * @param link The link that is to be resolved. * @param token A cancellation token. */ resolveDocumentLink?(link: DocumentLink, token: CancellationToken): ProviderResult } /** * A code lens provider adds [commands](#Command) to source text. The commands will be shown * as dedicated horizontal lines in between the source text. */ export interface CodeLensProvider { /** * Compute a list of [lenses](#CodeLens). This call should return as fast as possible and if * computing the commands is expensive implementors should only return code lens objects with the * range set and implement [resolve](#CodeLensProvider.resolveCodeLens). * * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of code lenses or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideCodeLenses(document: LinesTextDocument, token: CancellationToken): ProviderResult /** * This function will be called for each visible code lens, usually when scrolling and after * calls to [compute](#CodeLensProvider.provideCodeLenses)-lenses. * * @param codeLens code lens that must be resolved. * @param token A cancellation token. * @return The given, resolved code lens or thenable that resolves to such. */ resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult } /** * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ export interface OnTypeFormattingEditProvider { /** * Provide formatting edits after a character has been typed. * * The given position and character should hint to the provider * what range the position to expand to, like find the matching `{` * when `}` has been entered. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param ch The character that has been typed. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ provideOnTypeFormattingEdits(document: LinesTextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): ProviderResult } /** * The document color provider defines the contract between extensions and feature of * picking and modifying colors in the editor. */ export interface DocumentColorProvider { /** * Provide colors for the given document. * * @param document The document in which the command was invoked. * @param token A cancellation token. * @return An array of [color information](#ColorInformation) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideDocumentColors(document: LinesTextDocument, token: CancellationToken): ProviderResult /** * Provide [representations](#ColorPresentation) for a color. * * @param color The color to show and insert. * @param context A context object with additional information * @param token A cancellation token. * @return An array of color presentations or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ provideColorPresentations(color: Color, context: { document: LinesTextDocument; range: Range }, token: CancellationToken): ProviderResult } export interface TextDocumentContentProvider { /** * An event to signal a resource has changed. */ onDidChange?: Event /** * Provide textual content for a given uri. * * The editor will use the returned string-content to create a readonly * [document](#LinesTextDocument). Resources allocated should be released when * the corresponding document has been [closed](#workspace.onDidCloseTextDocument). * * @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for. * @param token A cancellation token. * @return A string or a thenable that resolves to such. */ provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult } export interface SelectionRangeProvider { /** * Provide selection ranges starting at a given position. The first range must [contain](#Range.contains) * position and subsequent ranges must contain the previous range. */ provideSelectionRanges(document: LinesTextDocument, positions: Position[], token: CancellationToken): ProviderResult } /** * The call hierarchy provider interface describes the contract between extensions * and the call hierarchy feature which allows to browse calls and caller of function, * methods, constructor etc. */ export interface CallHierarchyProvider { /** * Bootstraps call hierarchy by returning the item that is denoted by the given document * and position. This item will be used as entry into the call graph. Providers should * return `undefined` or `null` when there is no item at the given location. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @returns A call hierarchy item or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ prepareCallHierarchy(document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult /** * Provide all incoming calls for an item, e.g all callers for a method. In graph terms this describes directed * and annotated edges inside the call graph, e.g the given item is the starting node and the result is the nodes * that can be reached. * * @param item The hierarchy item for which incoming calls should be computed. * @param token A cancellation token. * @returns A set of incoming calls or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideCallHierarchyIncomingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult /** * Provide all outgoing calls for an item, e.g call calls to functions, methods, or constructors from the given item. In * graph terms this describes directed and annotated edges inside the call graph, e.g the given item is the starting * node and the result is the nodes that can be reached. * * @param item The hierarchy item for which outgoing calls should be computed. * @param token A cancellation token. * @returns A set of outgoing calls or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult } /** * The document semantic tokens provider interface defines the contract between extensions and * semantic tokens. */ export interface DocumentSemanticTokensProvider { /** * An optional event to signal that the semantic tokens from this provider have changed. */ onDidChangeSemanticTokens?: Event /** * Tokens in a file are represented as an array of integers. The position of each token is expressed relative to * the token before it, because most tokens remain stable relative to each other when edits are made in a file. * * --- * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices: * * - at index `5*i` - `deltaLine`: token line number, relative to the previous token * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line) * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline. * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes`. We currently ask that `tokenType` < 65536. * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers` * * --- * ### How to encode tokens * * Here is an example for encoding a file with 3 tokens in a uint32 array: * ``` * { line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, * { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, * { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } * ``` * * 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types. * For this example, we will choose the following legend which must be passed in when registering the provider: * ``` * tokenTypes: ['property', 'type', 'class'], * tokenModifiers: ['private', 'static'] * ``` * * 2. The first transformation step is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked * up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags, * so a `tokenModifier` value of `3` is first viewed as binary `0b00000011`, which means `[tokenModifiers[0], tokenModifiers[1]]` because * bits 0 and 1 are set. Using this legend, the tokens now are: * ``` * { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, * { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 }, * { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } * ``` * * 3. The next step is to represent each token relative to the previous token in the file. In this case, the second token * is on the same line as the first token, so the `startChar` of the second token is made relative to the `startChar` * of the first token, so it will be `10 - 5`. The third token is on a different line than the second token, so the * `startChar` of the third token will not be altered: * ``` * { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, * { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 }, * { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } * ``` * * 4. Finally, the last step is to inline each of the 5 fields for a token in a single array, which is a memory friendly representation: * ``` * // 1st token, 2nd token, 3rd token * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * * @see [SemanticTokensBuilder](#SemanticTokensBuilder) for a helper to encode tokens as integers. * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. */ provideDocumentSemanticTokens(document: LinesTextDocument, token: CancellationToken): ProviderResult /** * Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement * this method (`provideDocumentSemanticTokensEdits`) and then return incremental updates to the previously provided semantic tokens. * * --- * ### How tokens change when the document changes * * Suppose that `provideDocumentSemanticTokens` has previously returned the following semantic tokens: * ``` * // 1st token, 2nd token, 3rd token * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * * Also suppose that after some edits, the new semantic tokens in a file are: * ``` * // 1st token, 2nd token, 3rd token * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` * It is possible to express these new tokens in terms of an edit applied to the previous tokens: * ``` * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens * * edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3 * ``` * * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again. * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. */ provideDocumentSemanticTokensEdits?(document: LinesTextDocument, previousResultId: string, token: CancellationToken): ProviderResult } /** * The document range semantic tokens provider interface defines the contract between extensions and * semantic tokens. */ export interface DocumentRangeSemanticTokensProvider { /** * @see [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). */ provideDocumentRangeSemanticTokens(document: LinesTextDocument, range: Range, token: CancellationToken): ProviderResult } export interface LinkedEditingRangeProvider { /** * For a given position in a document, returns the range of the symbol at the position and all ranges * that have the same content. A change to one of the ranges can be applied to all other ranges if the new content * is valid. An optional word pattern can be returned with the result to describe valid contents. * If no result-specific word pattern is provided, the word pattern from the language configuration is used. * * @param document The document in which the provider was invoked. * @param position The position at which the provider was invoked. * @param token A cancellation token. * @return A list of ranges that can be edited together */ provideLinkedEditingRanges(document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } /** * The inlay hints provider interface defines the contract between extensions and * the inlay hints feature. */ export interface InlayHintsProvider { /** * An optional event to signal that inlay hints from this provider have changed. */ onDidChangeInlayHints?: Event /** * Provide inlay hints for the given range and document. * * *Note* that inlay hints that are not {@link Range.contains contained} by the given range are ignored. * * @param document The document in which the command was invoked. * @param range The range for which inlay hints should be computed. * @param token A cancellation token. * @return An array of inlay hints or a thenable that resolves to such. */ provideInlayHints(document: LinesTextDocument, range: Range, token: CancellationToken): ProviderResult /** * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.textEdits text edits}, * or complete label {@link InlayHintLabelPart parts}. * * *Note* that the editor will resolve an inlay hint at most once. * * @param hint An inlay hint. * @param token A cancellation token. * @return The resolved inlay hint or a thenable that resolves to such. It is OK to return the given `item`. When no result is returned, the given `item` will be used. */ resolveInlayHint?(hint: T, token: CancellationToken): ProviderResult } /** * The type hierarchy provider interface describes the contract between extensions * and the type hierarchy feature. */ export interface TypeHierarchyProvider { /** * Bootstraps type hierarchy by returning the item that is denoted by the given document * and position. This item will be used as entry into the type graph. Providers should * return `undefined` or `null` when there is no item at the given location. * * @param document The document in which the command was invoked. * @param position The position at which the command was invoked. * @param token A cancellation token. * @returns One or multiple type hierarchy items or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ prepareTypeHierarchy(document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult /** * Provide all supertypes for an item, e.g all types from which a type is derived/inherited. In graph terms this describes directed * and annotated edges inside the type graph, e.g the given item is the starting node and the result is the nodes * that can be reached. * * @param item The hierarchy item for which super types should be computed. * @param token A cancellation token. * @returns A set of direct supertypes or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideTypeHierarchySupertypes(item: TypeHierarchyItem, token: CancellationToken): ProviderResult /** * Provide all subtypes for an item, e.g all types which are derived/inherited from the given item. In * graph terms this describes directed and annotated edges inside the type graph, e.g the given item is the starting * node and the result is the nodes that can be reached. * * @param item The hierarchy item for which subtypes should be computed. * @param token A cancellation token. * @returns A set of direct subtypes or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideTypeHierarchySubtypes(item: TypeHierarchyItem, token: CancellationToken): ProviderResult } /** * The inline values provider interface defines the contract between extensions and the editor's debugger inline values feature. * In this contract the provider returns inline value information for a given document range * and the editor shows this information in the editor at the end of lines. */ export interface InlineValuesProvider { /** * An optional event to signal that inline values have changed. * @see {@link EventEmitter} */ onDidChangeInlineValues?: Event | undefined /** * Provide "inline value" information for a given document and range. * The editor calls this method whenever debugging stops in the given document. * The returned inline values information is rendered in the editor at the end of lines. * @param document The document for which the inline values information is needed. * @param viewPort The visible document range for which inline values should be computed. * @param context A bag containing contextual information like the current location. * @param token A cancellation token. * @return An array of InlineValueDescriptors or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): ProviderResult } export interface DiagnosticProvider { onDidChangeDiagnostics: Event | undefined provideDiagnostics(document: TextDocument | Uri, previousResultId: string | undefined, token: CancellationToken): ProviderResult provideWorkspaceDiagnostics?(resultIds: PreviousResultId[], token: CancellationToken, resultReporter: ResultReporter): ProviderResult } /** * The inline completion item provider interface defines the contract between extensions and * the inline completion feature. * * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. */ export interface InlineCompletionItemProvider { /** * Provides inline completion items for the given position and document. * If inline completions are enabled, this method will be called whenever the user stopped typing. * It will also be called when the user explicitly triggers inline completions or explicitly asks for the next or previous inline completion. * In that case, all available inline completions should be returned. * `context.triggerKind` can be used to distinguish between these scenarios. * @param document The document inline completions are requested for. * @param position The position inline completions are requested for. * @param context A context object with additional information. * @param token A cancellation token. * @returns An array of completion items or a thenable that resolves to an array of completion items. */ provideInlineCompletionItems( document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken ): ProviderResult } // }} // Classes {{ /** * An error type that should be used to signal cancellation of an operation. * * This type can be used in response to a {@link CancellationToken cancellation token} * being cancelled or when an operation is being cancelled by the * executor of that operation. */ export class CancellationError extends Error { /** * Creates a new cancellation error. */ constructor() } /** * A semantic tokens builder can help with creating a `SemanticTokens` instance * which contains delta encoded semantic tokens. */ export class SemanticTokensBuilder { constructor(legend?: SemanticTokensLegend) /** * Add another token. * * @public * @param line The token start line number (absolute value). * @param char The token start character (absolute value). * @param length The token length in characters. * @param tokenType The encoded token type. * @param tokenModifiers The encoded token modifiers. */ push(line: number, char: number, length: number, tokenType: number, tokenModifiers?: number): void /** * Add another token. Use only when providing a legend. * * @public * @param range The range of the token. Must be single-line. * @param tokenType The token type. * @param tokenModifiers The token modifiers. */ push(range: Range, tokenType: string, tokenModifiers?: string[]): void /** * Finish and create a `SemanticTokens` instance. * * @public */ build(resultId?: string): SemanticTokens } export interface Document { readonly buffer: Buffer /** * Document is attached to vim. */ readonly attached: boolean /** * Is command line document. */ readonly isCommandLine: boolean /** * `buftype` option of buffer. */ readonly buftype: string /** * Text document that synchronized. */ readonly textDocument: LinesTextDocument /** * Fired when document change. */ readonly onDocumentChange: Event /** * Get current buffer changedtick. */ readonly changedtick: number /** * Scheme of document. */ readonly schema: string /** * Line count of current buffer. */ readonly lineCount: number /** * Window ID when buffer create, could be -1 when no window associated. */ readonly winid: number /** * Returns if current document is opened with previewwindow */ readonly previewwindow: boolean /** * Check if document changed after last synchronize */ readonly dirty: boolean /** * Buffer number */ readonly bufnr: number /** * Content of textDocument. */ readonly content: string /** * Converted filetype. */ readonly filetype: string /** * Main filetype of buffer, first part when buffer filetype contains dots. * Same as filetype most of the time. */ readonly languageId: string readonly uri: string readonly version: number /** * Current lines of buffer */ readonly lines: ReadonlyArray /** * Apply text edits to document. `nvim_buf_set_text()` is used when possible * * @param {TextEdit[]} edits * @param {boolean} joinUndo - Join further changes with the previous undo block by `:undojoin`. * @param {boolean | Position} move - Move the cursor when true or from custom position. * @returns {Promise} */ applyEdits(edits: TextEdit[], joinUndo?: boolean, move?: boolean | Position): Promise /** * Change individual lines. * * @param {[number, string][]} lines * @returns {void} */ changeLines(lines: [number, string][]): Promise /** * Get offset from lnum & col */ getOffset(lnum: number, col: number): number /** * Check string is word. */ isWord(word: string): boolean /** * Word range at position. * * @param {Position} position * @param {string} extraChars Extra characters that should be keyword. * @param {boolean} current Use current lines instead of textDocument, default to true. * @returns {Range | null} */ getWordRangeAtPosition(position: Position, extraChars?: string, current?: boolean): Range | null /** * Get ranges of word in textDocument. */ getSymbolRanges(word: string): Range[] /** * Get line for buffer * * @param {number} line 0 based line index. * @param {boolean} current Use textDocument lines when false, default to true. * @returns {string} */ getline(line: number, current?: boolean): string /** * Get range of current lines, zero indexed, end exclude. */ getLines(start?: number, end?: number): string[] /** * Get variable value by key, defined by `b:coc_{key}` */ getVar(key: string, defaultValue?: T): T /** * Get position from lnum & col */ getPosition(lnum: number, col: number): Position /** * Adjust col with new valid character before position. */ fixStartcol(position: Position, valids: string[]): number /** * Get current content text, consider eol option. */ getDocumentContent(): string } /** * Represents a {@link TextEditor text editor}'s {@link TextEditor.options options}. */ export interface TextEditorOptions { /** * The size in spaces a tab takes. This is used for two purposes: * - the rendering width of a tab character; * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true. * * When getting a text editor's options, this property will always be a number (resolved). */ tabSize: number /** * When pressing Tab insert {@link TextEditorOptions.tabSize n} spaces. * When getting a text editor's options, this property will always be a boolean (resolved). */ insertSpaces: boolean /** * Trim trailing whitespace on a line. */ trimTrailingWhitespace?: boolean /** * Insert a newline character at the end of the file if one does not exist. */ insertFinalNewline?: boolean /** * Trim all newlines after the final newline at the end of the file. */ trimFinalNewlines?: boolean } /** * Represents an editor that is attached to a {@link Document document}. */ export interface TextEditor { /** * The tabpageid of current editor. */ readonly tabpageid: number /** * The window id of current editor. */ readonly winid: number /** * The window number of current editor. */ readonly winnr: number /** * The document associated with this text editor. The document will be the same for the entire lifetime of this text editor. */ readonly document: Document /** * The current visible ranges in the editor (vertically). * This accounts only for vertical scrolling, and not for horizontal scrolling. */ readonly visibleRanges: readonly Range[] /** * Text editor options. */ readonly options: TextEditorOptions } export interface Documentation { /** * Filetype used for highlight, markdown is supported. */ filetype: string /** * Content of document. */ content: string /** * Byte offset (0 based) that should be undelined. */ active?: [number, number] highlights?: HighlightItem[] } /** * A file glob pattern to match file paths against. This can either be a glob pattern string * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a {@link RelativePattern relative pattern}. * * Glob patterns can have the following syntax: * * `*` to match one or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) * * Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file * path to match against, consider to use the {@link RelativePattern relative pattern} support * that takes care of converting any backslash into slash. Otherwise, make sure to convert * any backslash to slash when creating the glob pattern. */ export type GlobPattern = string | RelativePattern /** * A relative pattern is a helper to construct glob patterns that are matched * relatively to a base file path. The base path can either be an absolute file * path as string or uri or a {@link WorkspaceFolder workspace folder}, which is the * preferred way of creating the relative pattern. */ export class RelativePattern { /** * A base file path to which this pattern will be matched against relatively. */ baseUri: Uri /** * A file glob pattern like `*.{ts,js}` that will be matched on file paths * relative to the base path. * * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`, * the file glob pattern will match on `index.js`. */ pattern: string /** * Creates a new relative pattern object with a base file path and pattern to match. This pattern * will be matched on file paths relative to the base. * * Example: * ```ts * const folder = vscode.workspace.workspaceFolders?.[0]; * if (folder) { * * // Match any TypeScript file in the root of this workspace folder * const pattern1 = new vscode.RelativePattern(folder, '*.ts'); * * // Match any TypeScript file in `someFolder` inside this workspace folder * const pattern2 = new vscode.RelativePattern(folder, 'someFolder/*.ts'); * } * ``` * * @param base A base to which this pattern will be matched against relatively. It is recommended * to pass in a {@link WorkspaceFolder workspace folder} if the pattern should match inside the workspace. * Otherwise, a uri or string should only be used if the pattern is for a file path outside the workspace. * @param pattern A file glob pattern like `*.{ts,js}` that will be matched on paths relative to the base. */ constructor(base: WorkspaceFolder | Uri | string, pattern: string) } /** * Build buffer with lines and highlights */ export class Highlighter { constructor(srcId?: number) /** * Add a line with highlight group. */ addLine(line: string, hlGroup?: string): void /** * Add lines without highlights. */ addLines(lines: string[]): void /** * Add text with highlight. */ addText(text: string, hlGroup?: string): void /** * Get line count */ get length(): number /** * Render lines to buffer at specified range. * Since notifications is used, use `nvim.pauseNotification` & `nvim.resumeNotification` * when you need to wait for the request finish. * * @param {Buffer} buffer * @param {number} start * @param {number} end * @returns {void} */ render(buffer: Buffer, start?: number, end?: number): void } export interface ListConfiguration { get(key: string, defaultValue?: T): T previousKey(): string nextKey(): string dispose(): void } export interface ListActionOptions { /** * No prompt stop and window switch when invoked. */ persist?: boolean /** * Reload list after action invoked. */ reload?: boolean /** * Support multiple items as execute argument. */ parallel?: boolean /** * Tab positioned list should be persisted (no window switch) on action invoke. */ tabPersist?: boolean } export interface CommandTaskOption { /** * Command to run. */ cmd: string /** * Arguments of command. */ args: string[] /** * Current working directory. */ cwd?: string env?: NodeJS.ProcessEnv /** * Runs for each line, return undefined for invalid item. */ onLine: (line: string) => ListItem | undefined } export abstract class BasicList implements IList { /** * Unique name, must be provided by implementation class. * @requires */ name: string /** * Default action name invoked by by default, must be provided by implementation class. * @requires */ defaultAction: string /** * Registered actions. */ readonly actions: ListAction[] /** * Arguments configuration of list. */ options: ListArgument[] protected nvim: Neovim protected disposables: Disposable[] protected config: ListConfiguration constructor() /** * Should align columns when true. */ get alignColumns(): boolean get hlGroup(): string get previewHeight(): string get splitRight(): boolean /** * Parse argument string array for argument object from `this.options`. * Could be used inside `this.loadItems()` */ protected parseArguments(args: string[]): { [key: string]: string | boolean } /** * Get configurations of current list */ protected getConfig(): WorkspaceConfiguration /** * Add an action */ protected addAction(name: string, fn: (item: ListItem, context: ListContext) => ProviderResult, options?: ListActionOptions): void /** * Add action that support multiple selection. */ protected addMultipleAction(name: string, fn: (item: ListItem[], context: ListContext) => ProviderResult, options?: ListActionOptions): void /** * Create task from command task option. */ protected createCommandTask(opt: CommandTaskOption): ListTask /** * Add location related actions, should be called in constructor. */ protected addLocationActions(): void protected convertLocation(location: Location | LocationWithLine | string): Promise /** * Jump to location */ protected jumpTo(location: Location | LocationWithLine | string, command?: string): Promise /** * Preview location. */ protected previewLocation(location: Location, context: ListContext): Promise /** * Preview lines. */ protected preview(options: PreviewOptions, context: ListContext): Promise /** * Use for syntax highlights, invoked after buffer loaded. */ doHighlight(): void /** * Invoked for listItems or listTask, could throw error when failed to load. */ abstract loadItems(context: ListContext, token?: CancellationToken): Promise } export class Mutex { /** * Returns true when task is running. */ get busy(): boolean /** * Resolved release function that must be called after task finish. */ acquire(): Promise<() => void> /** * Captrue the async task function that ensures to be executed one by one. */ use(f: () => Promise): Promise } // }} // functions {{ export interface AnsiItem { foreground?: string background?: string bold?: boolean italic?: boolean underline?: boolean text: string } export interface ParsedUrlQueryInput { [key: string]: unknown } export interface FetchOptions { /** * Default to 'GET' */ method?: string /** * Default no timeout */ timeout?: number /** * Always return buffer instead of parsed response. */ buffer?: boolean /** * Data send to server. */ data?: string | { [key: string]: any } | Buffer /** * Plain object added as query of url */ query?: ParsedUrlQueryInput headers?: any /** * User for http basic auth, should use with password */ user?: string /** * Password for http basic auth, should use with user */ password?: string } export interface DownloadOptions extends Omit { /** * Folder that contains downloaded file or extracted files by untar or unzip */ dest: string /** * Remove the specified number of leading path elements for *untar* only, default to `1`. */ strip?: number /** * algorithm for check etag header with response data, used by `crypto.createHash()`. */ etagAlgorithm?: string /** * If true, use untar for `.tar.gz` filename */ extract?: boolean | 'untar' | 'unzip' onProgress?: (percent: string) => void } export type ResponseResult = string | Buffer | { [name: string]: any } /** * Parse ansi result from string contains ansi characters. */ export function ansiparse(str: string): AnsiItem[] /** * Send request to server for response, supports: * * - Send json data and parse json response. * - Throw error for failed response statusCode. * - Timeout support (no timeout by default). * - Send buffer (as data) and receive data (as response). * - Proxy support from user configuration & environment. * - Redirect support, limited to 3. * - Support of gzip & deflate response content. * * @return Parsed object if response content type is application/json, text if content type starts with `text/` */ export function fetch(url: string | URL, options?: FetchOptions, token?: CancellationToken): Promise /** * Download file from url, with optional untar/unzip support. * * Note: you may need to set `strip` to 0 when using untar as extract method. * * @param {string} url * @param {DownloadOptions} options contains dest folder and optional onProgress callback */ export function download(url: string | URL, options: DownloadOptions, token?: CancellationToken): Promise interface ExecOptions { cwd?: string env?: NodeJS.ProcessEnv shell?: string timeout?: number maxBuffer?: number killSignal?: string uid?: number gid?: number windowsHide?: boolean encoding?: string } /** * Dispose all disposables. */ export function disposeAll(disposables: Disposable[]): void /** * Concurrent run async functions with limit support. */ export function concurrent(arr: T[], fn: (val: T) => Promise, limit?: number): Promise /** * Create promise resolved after ms milliseconds. */ export function wait(ms: number): Promise /** * Run command with `child_process.exec`, CancellationError is rejected when timeout or cancelled. * * @param {string} cmd * @param {ExecOptions} opts - Execute options, encoding is used by * iconv-lite for decode stdout buffer to string, default to 'utf8' * @param {number | CancellationToken} timeout - Timeout in seconds or Cancellation token. * @returns {Promise} */ export function runCommand(cmd: string, opts?: ExecOptions, timeout?: number | CancellationToken): Promise /** * Check if process with pid is running */ export function isRunning(pid: number): boolean /** * Check if command is executable. */ export function executable(command: string): boolean /** * Watch single file for change, the filepath needs to be exists file. * * @param filepath Full path of file. * @param onChange Handler on file change detected. */ export function watchFile(filepath: string, onChange: () => void): Disposable // }} // commands module {{ export interface CommandItem { id: string internal?: boolean execute(...args: any[]): any } /** * Namespace for dealing with commands of coc.nvim */ export namespace commands { /** * Registered commands. */ export const commandList: CommandItem[] /** * Execute specified command. * * @deprecated use `executeCommand()` instead. */ export function execute(command: { name: string, arguments?: any[] }): void /** * Check if command is registered. * * @param id Unique id of command. */ export function has(id: string): boolean /** * Registers a command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. * * Registering a command with an existing command identifier twice * will cause an error. * * @param command A unique identifier for the command. * @param impl A command handler function. * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ export function registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any, internal?: boolean): Disposable /** * Executes the command denoted by the given command identifier. * * * *Note 1:* When executing an editor command not all types are allowed to * be passed as arguments. Allowed are the primitive types `string`, `boolean`, * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`URI`](#URI) and [`Location`](#Location). * * *Note 2:* There are no restrictions when executing commands that have been contributed * by extensions. * * @param command Identifier of the command to execute. * @param rest Parameters passed to the command function. * @return A promise that resolves to the returned value of the given command. `undefined` when * the command handler function doesn't return anything. */ export function executeCommand(command: string, ...rest: any[]): Promise /** * Open uri with external tool, use `open` on mac, use `xdg-open` on linux. */ export function executeCommand(command: 'vscode.open', uri: string | Uri): Promise /** * Reload current buffer by `:edit` command. */ export function executeCommand(command: 'workbench.action.reloadWindow'): Promise /** * Open user's coc-settings.json configuration file. */ export function executeCommand(command: 'workbench.action.openSettingsJson'): Promise /** * Insert snippet at range of current buffer. * * @param edit Contains snippet text and range to replace. */ export function executeCommand(command: 'editor.action.insertSnippet', edit: TextEdit, ultisnip?: UltiSnippetOption): Promise /** * Invoke specified code action. */ export function executeCommand(command: 'editor.action.doCodeAction', action: CodeAction): Promise /** * Trigger coc.nvim's completion at current cursor position. */ export function executeCommand(command: 'editor.action.triggerSuggest', source?: string): Promise /** * Trigger signature help at current cursor position. */ export function executeCommand(command: 'editor.action.triggerParameterHints'): Promise /** * Add ranges to cursors session for multiple cursors. */ export function executeCommand(command: 'editor.action.addRanges', ranges: Range[]): Promise /** * Restart coc.nvim service by `:CocRestart` command. */ export function executeCommand(command: 'editor.action.restart'): Promise /** * Show locations by location list or vim's quickfix list. */ export function executeCommand(command: 'editor.action.showReferences', uri: string | Uri, position: Position | undefined, locations: Location[]): Promise /** * Invoke rename action at position of specified uri. */ export function executeCommand(command: 'editor.action.rename', uri: string | Uri, position: Position, newName?: string): Promise /** * Run format action for current buffer. */ export function executeCommand(command: 'editor.action.format'): Promise /** * Trigger inline completion */ export function executeCommand(command: 'editor.action.triggerInlineCompletion', option?: InlineCompletionOption): Promise } // }} // events module {{ type EventResult = void | Promise type MoveEvents = 'CursorMoved' | 'CursorMovedI' type HoldEvents = 'CursorHold' | 'CursorHoldI' type BufEvents = 'BufHidden' | 'BufEnter' | 'BufWritePost' | 'InsertLeave' | 'TermOpen' | 'InsertEnter' | 'BufCreate' | 'BufUnload' | 'BufWritePre' | 'Enter' type EmptyEvents = 'FocusGained' | 'FocusLost' | 'InsertSnippet' type InsertChangeEvents = 'TextChangedP' | 'TextChangedI' type TaskEvents = 'TaskExit' | 'TaskStderr' | 'TaskStdout' type WindowEvents = 'WinLeave' | 'WinEnter' | 'WinClosed' type AllEvents = BufEvents | EmptyEvents | HoldEvents | MoveEvents | TaskEvents | WindowEvents | InsertChangeEvents | 'CompleteDone' | 'TextChanged' | 'MenuPopupChanged' | 'InsertCharPre' | 'FileType' | 'BufWinEnter' | 'BufWinLeave' | 'VimResized' | 'DirChanged' | 'OptionSet' | 'Command' | 'BufReadCmd' | 'GlobalChange' | 'InputChar' | 'WinLeave' | 'MenuInput' | 'PromptInsert' | 'FloatBtnClick' | 'InsertSnippet' | 'PromptKeyPress' | 'WinScrolled' | 'WindowVisible' type OptionValue = string | number | boolean type PromptWidowKeys = 'C-j' | 'C-k' | 'C-n' | 'C-p' | 'up' | 'down' export interface CursorPosition { readonly bufnr: number readonly lnum: number readonly col: number readonly insert: boolean } export interface InsertChange { /** * 1 based line number */ readonly lnum: number /** * 1 based column number */ readonly col: number /** * Text before cursor. */ readonly pre: string /** * Insert character that cause change of this time. */ readonly insertChar: string | undefined readonly changedtick: number } export interface PopupChangeEvent { /** * 0 based index of item in the list. */ readonly index: number /** * Word of item. */ readonly word: string /** * Height of pum. */ readonly height: number /** * Width of pum. */ readonly width: number /** * Screen row of pum. */ readonly row: number /** * Screen col of pum. */ readonly col: number /** * Total length of completion list. */ readonly size: number /** * Scollbar in the pum. */ readonly scrollbar: boolean /** * Word is inserted. */ readonly inserted: boolean /** * Caused by selection change (not initial or completed) */ readonly move: boolean } export interface VisibleEvent { winid: number bufnr: number /** * 1 based, end inclusive topline, botline */ region: [number, number] } /** * Used for listen to events send from vim. */ export namespace events { /** * Latest cursor position. */ export const cursor: Readonly /** * Latest pum position, is true when pum positioned above current line. */ export const pumAlignTop: boolean /** * Insert mode detected by latest events. */ export const insertMode: boolean /** * Popup menu is visible. */ export const pumvisible: boolean /** * Wait for any of event in events to fire, resolve undefined when timeout or CancellationToken requested. * @param events Event names to wait. * @param timeoutOrToken Timeout in miniseconds or CancellationToken. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function race(events: AllEvents[], timeoutOrToken?: number | CancellationToken): Promise<{ name: AllEvents, args: unknown[] } | undefined> /** * Attach handler to buffer events. */ export function on(event: BufEvents, handler: (bufnr: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to mouse move events. */ export function on(event: MoveEvents, handler: (bufnr: number, cursor: [number, number], hasInsert: boolean) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to cursor hold events. */ export function on(event: HoldEvents, handler: (bufnr: number, cursor: [number, number], winid: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to TextChangedI or TextChangedP. */ export function on(event: InsertChangeEvents, handler: (bufnr: number, info: InsertChange) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to window event. */ export function on(event: WindowEvents, handler: (winid: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to window scroll event. */ export function on(event: WindowEvents, handler: (winid: number, bufnr: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to float button click. */ export function on(event: 'FloatBtnClick', handler: (bufnr: number, index: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Attach handler to keypress in prompt window. * Key could only be 'C-j', 'C-k', 'C-n', 'C-p', 'up' and 'down' */ export function on(event: 'PromptKeyPress', handler: (bufnr: number, key: PromptWidowKeys) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Fired on vim's TextChanged event. */ export function on(event: 'TextChanged', handler: (bufnr: number, changedtick: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'TaskExit', handler: (id: string, code: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'TaskStderr' | 'TaskStdout', handler: (id: string, lines: string[]) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Fired on vim's BufReadCmd event. */ export function on(event: 'BufReadCmd', handler: (scheme: string, fullpath: string) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Fired on vim's VimResized event. */ export function on(event: 'VimResized', handler: (columns: number, lines: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'MenuPopupChanged', handler: (event: PopupChangeEvent, cursorline: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Fired on completion finish. */ export function on(event: 'CompleteDone', handler: (item: VimCompleteItem & CompleteDoneItem | CompletionItem & CompleteDoneItem | {}) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Fired on completion start. */ export function on(event: 'CompleteStart', handler: (option: CompleteOption) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'InsertCharPre', handler: (character: string) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'FileType', handler: (filetype: string, bufnr: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'BufWinEnter' | 'BufWinLeave', handler: (bufnr: number, winid: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'DirChanged', handler: (cwd: string) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'OptionSet' | 'GlobalChange', handler: (option: string, oldVal: OptionValue, newVal: OptionValue) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'InputChar', handler: (session: string, character: string, mode: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'PromptInsert', handler: (value: string, bufnr: number) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: 'Command', handler: (name: string) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Emitted on WinScrolled event of vim, with related `winid` and `bufnr`, * `region` contains [topline, botline] which are 1 based, end enclusive * (the same as the result from getwininfo()). */ export function on(event: 'WinScrolled', handler: (winid: number, bufnr: number, region: Readonly<[number, number]>) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Emitted with debounce time (100ms) after `BufWinEnter` and `WinScrolled` * for the change of window visible regions. To track visible changes of all * attached buffers, use `workspace.registerBufferSync()` is recommended. */ export function on(event: 'WindowVisible', handler: (event: VisibleEvent) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable /** * Fired after user insert character and made change to the buffer. * Fired after TextChangedI & TextChangedP event. */ export function on(event: 'TextInsert', handler: (bufnr: number, info: InsertChange, character: string) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: EmptyEvents, handler: () => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable export function on(event: AllEvents[], handler: (...args: unknown[]) => EventResult, thisArg?: any, disposables?: Disposable[]): Disposable } // }} // file events {{ /** * An event that is fired after files are created. */ export interface FileCreateEvent { /** * The files that got created. */ readonly files: ReadonlyArray } /** * An event that is fired when files are going to be created. * * To make modifications to the workspace before the files are created, * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a * thenable that resolves to a [workspace edit](#WorkspaceEdit). */ export interface FileWillCreateEvent { /** * A cancellation token. */ readonly token: CancellationToken /** * The files that are going to be created. */ readonly files: ReadonlyArray /** * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillCreateFiles(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * * @param thenable A thenable that delays saving. */ waitUntil(thenable: Thenable): void } /** * An event that is fired when files are going to be deleted. * * To make modifications to the workspace before the files are deleted, * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a * thenable that resolves to a [workspace edit](#WorkspaceEdit). */ export interface FileWillDeleteEvent { /** * The files that are going to be deleted. */ readonly files: ReadonlyArray /** * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillCreateFiles(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * * @param thenable A thenable that delays saving. */ waitUntil(thenable: Thenable): void } /** * An event that is fired after files are deleted. */ export interface FileDeleteEvent { /** * The files that got deleted. */ readonly files: ReadonlyArray } /** * An event that is fired after files are renamed. */ export interface FileRenameEvent { /** * The files that got renamed. */ readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }> } /** * An event that is fired when files are going to be renamed. * * To make modifications to the workspace before the files are renamed, * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a * thenable that resolves to a [workspace edit](#WorkspaceEdit). */ export interface FileWillRenameEvent { /** * The files that are going to be renamed. */ readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }> /** * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillCreateFiles(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * * @param thenable A thenable that delays saving. */ waitUntil(thenable: Thenable): void } // }} // languages module {{ export interface DocumentSymbolProviderMetadata { /** * A human-readable string that is shown when multiple outlines trees show for one document. */ label?: string } export namespace languages { /** * Check if specific provider exists for document. */ export function hasProvider(id: ProviderName, document: TextDocumentMatch): boolean /** * Create a diagnostics collection. * * @param name The [name](#DiagnosticCollection.name) of the collection. * @return A new diagnostic collection. */ export function createDiagnosticCollection(name?: string): DiagnosticCollection /** * Register a formatting provider that works on type. The provider is active when the user enables the setting `coc.preferences.formatOnType`. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#languages.match) and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An on type formatting edit provider. * @param triggerCharacters Trigger character that should trigger format on type. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerOnTypeFormattingEditProvider(selector: DocumentSelector, provider: OnTypeFormattingEditProvider, triggerCharacters: string[]): Disposable /** * Register a completion provider. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#languages.match) and groups of equal score are sequentially asked for * completion items. The process stops when one or many providers of a group return a * result. A failing provider (rejected promise or exception) will not fail the whole * operation. * * A completion item provider can be associated with a set of `triggerCharacters`. When trigger * characters are being typed, completions are requested but only from providers that registered * the typed character. Because of that trigger characters should be different than [word characters](#LanguageConfiguration.wordPattern), * a common trigger character is `.` to trigger member completions. * * @param name Name of completion source. * @param shortcut Shortcut used in completion menu. * @param selector Document selector of created completion source. * @param provider A completion provider. * @param triggerCharacters Trigger completion when the user types one of the characters. * @param priority Higher priority would shown first. * @param allCommitCharacters Commit characters of completion source. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerCompletionItemProvider(name: string, shortcut: string, selector: DocumentSelector | null, provider: CompletionItemProvider, triggerCharacters?: string[], priority?: number, allCommitCharacters?: string[]): Disposable /** * Register a code action provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code action provider. * @param clientId Optional id of language client. * @param codeActionKinds Optional supported code action kinds. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerCodeActionProvider(selector: DocumentSelector, provider: CodeActionProvider, clientId: string | undefined, codeActionKinds?: ReadonlyArray): Disposable /** * Register a hover provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A hover provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable /** * Register a selection range provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A selection range provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerSelectionRangeProvider(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable /** * Register a signature help provider. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#languages.match) and called sequentially until a provider returns a * valid result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A signature help provider. * @param triggerCharacters Trigger signature help when the user types one of the characters, like `,` or `(`. * @param metadata Information about the provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerSignatureHelpProvider(selector: DocumentSelector, provider: SignatureHelpProvider, triggerCharacters?: string[]): Disposable /** * Register a document symbol provider. * * Multiple providers can be registered for a language. In that case providers only first provider * are asked for result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document symbol provider. * @param metadata Optional meta data. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentSymbolProvider(selector: DocumentSelector, provider: DocumentSymbolProvider, metadata?: DocumentSymbolProviderMetadata): Disposable /** * Register a folding range provider. * * Multiple providers can be registered for a language. In that case providers only first provider * are asked for result. * * A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A folding range provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerFoldingRangeProvider(selector: DocumentSelector, provider: FoldingRangeProvider): Disposable /** * Register a document highlight provider. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#languages.match) and groups sequentially asked for document highlights. * The process stops when a provider returns a `non-falsy` or `non-failure` result. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document highlight provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentHighlightProvider(selector: DocumentSelector, provider: DocumentHighlightProvider): Disposable /** * Register a code lens provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code lens provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerCodeLensProvider(selector: DocumentSelector, provider: CodeLensProvider): Disposable /** * Register a document link provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document link provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentLinkProvider(selector: DocumentSelector, provider: DocumentLinkProvider): Disposable /** * Register a color provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A color provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable /** * Register a definition provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A definition provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDefinitionProvider(selector: DocumentSelector, provider: DefinitionProvider): Disposable /** * Register a declaration provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A declaration provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDeclarationProvider(selector: DocumentSelector, provider: DeclarationProvider): Disposable /** * Register a type definition provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A type definition provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerTypeDefinitionProvider(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable /** * Register an implementation provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An implementation provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerImplementationProvider(selector: DocumentSelector, provider: ImplementationProvider): Disposable /** * Register a reference provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A reference provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerReferencesProvider(selector: DocumentSelector, provider: ReferenceProvider): Disposable /** * Register a rename provider. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#workspace.match) and asked in sequence. The first provider producing a result * defines the result of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A rename provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable /** * Register a workspace symbol provider. * * Multiple providers can be registered. In that case providers are asked in parallel and * the results are merged. A failing provider (rejected promise or exception) will not cause * a failure of the whole operation. * * @param provider A workspace symbol provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerWorkspaceSymbolProvider(provider: WorkspaceSymbolProvider): Disposable /** * Register a formatting provider for a document. * * Multiple providers can be registered for a language. In that case providers are sorted * by their priority. Failure of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document formatting edit provider. * @param priority default to 0. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentFormatProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider, priority?: number): Disposable /** * Register a formatting provider for a document range. * * *Note:* A document range provider is also a [document formatter](#DocumentFormattingEditProvider) * which means there is no need to [register](#languages.registerDocumentFormattingEditProvider) a document * formatter when also registering a range provider. * * Multiple providers can be registered for a language. In that case provider with highest priority is used. * Failure of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range formatting edit provider. * @param priority default to 0. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentRangeFormatProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, priority?: number): Disposable /** * Register a call hierarchy provider. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A call hierarchy provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable /** * Register a semantic tokens provider for a whole document. * * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document semantic tokens provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable /** * Register a semantic tokens provider for a document range. * * *Note:* If a document has both a `DocumentSemanticTokensProvider` and a `DocumentRangeSemanticTokensProvider`, * the range provider will be invoked only initially, for the time in which the full document provider takes * to resolve the first request. Once the full document provider resolves the first request, the semantic tokens * provided via the range provider will be discarded and from that point forward, only the document provider * will be used. * * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link languages.match score} and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range semantic tokens provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable /** * Register a linked editing range provider. * * Multiple providers can be registered for a language. In that case providers are sorted * by their {@link languages.match score} and the best-matching provider that has a result is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A linked editing range provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerLinkedEditingRangeProvider(selector: DocumentSelector, provider: LinkedEditingRangeProvider): Disposable /** * Register a inlay hints provider. * * Multiple providers can be registered for a language. In that case providers are asked in * parallel and the results are merged. A failing provider (rejected promise or exception) will * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An inlay hints provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable /** * Register a type hierarchy provider. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A type hierarchy provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerTypeHierarchyProvider(selector: DocumentSelector, provider: TypeHierarchyProvider): Disposable /** * Register a inline completion item provider. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A InlineCompletion provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable } // }} // services module {{ export enum ServiceStat { Initial, Starting, StartFailed, Running, Stopping, Stopped, } export interface IServiceProvider { // unique service id id: string name: string client?: LanguageClient selector: DocumentSelector // current state state: ServiceStat start(): Promise dispose(): void stop(): Promise | void restart(): Promise | void onServiceReady: Event } export namespace services { /** * Register languageClient as service provider. */ export function registerLanguageClient(client: LanguageClient): Disposable /** * @deprecated use registerLanguageClient instead. */ export function registLanguageClient(client: LanguageClient): Disposable /** * Register service, nothing happens when `service.id` already exists. */ export function register(service: IServiceProvider): Disposable /** * @deprecated use register instead. */ export function regist(service: IServiceProvider): Disposable /** * Get service by id. */ export function getService(id: string): IServiceProvider /** * Stop service by id. */ export function stop(id: string): Promise /** * Stop running service or start stopped service. */ export function toggle(id: string): Promise } // }} // sources module {{ /** * Source options to create source that could respect configuration from `coc.source.{name}` */ export interface SourceConfig { name: string triggerOnly?: boolean isSnippet?: boolean sourceType?: SourceType filepath?: string documentSelector?: DocumentSelector firstMatch?: boolean refresh?(): Promise toggle?(): void onEnter?(bufnr: number): void shouldComplete?(opt: CompleteOption): ProviderResult doComplete(opt: CompleteOption, token: CancellationToken): ProviderResult onCompleteResolve?(item: VimCompleteItem, opt: CompleteOption, token: CancellationToken): ProviderResult onCompleteDone?(item: VimCompleteItem, opt: CompleteOption, snippetsSupport?: boolean): ProviderResult shouldCommit?(item: VimCompleteItem, character: string): boolean } export interface SourceStat { name: string priority: number triggerCharacters: string[] type: 'native' | 'remote' | 'service' shortcut: string filepath: string disabled: boolean filetypes: string[] } export enum SourceType { Native, Remote, Service, } export interface CompleteResult { items: ReadonlyArray isIncomplete?: boolean startcol?: number } // option on complete & should_complete export interface CompleteOption { /** * Current buffer number. */ readonly bufnr: number /** * Current line. */ readonly line: string /** * Column to start completion, determined by iskeyword options of buffer. */ readonly col: number /** * Input text. */ readonly input: string readonly filetype: string readonly filepath: string /** * Word under cursor. */ readonly word: string /** * Trigger character, could be undefined. */ readonly triggerCharacter?: string /** * Col of cursor, 1 based. */ readonly colnr: number readonly linenr: number /** * Position of cursor when trigger completion */ readonly position: Position readonly synname: string /** * Buffer changetick */ readonly changedtick: number /** * Is trigger for in complete completion. */ readonly triggerForInComplete?: boolean } export interface ISource { /** * Identifier name */ name: string /** * @deprecated use documentSelector instead. */ filetypes?: string[] /** * Filters of document. */ documentSelector?: DocumentSelector enable?: boolean shortcut?: string priority?: number sourceType?: SourceType /** * Should only be used when completion is triggered, requires `triggerPatterns` or `triggerCharacters` defined. */ triggerOnly?: boolean triggerCharacters?: string[] // regex to detect trigger completion, ignored when triggerCharacters exists. triggerPatterns?: RegExp[] disableSyntaxes?: string[] filepath?: string // should the first character always match firstMatch?: boolean refresh?(): Promise /** * For disable/enable */ toggle?(): void /** * Triggered on BufEnter, used for cache normally */ onEnter?(bufnr: number): void /** * Check if this source should doComplete * * @public * @param {CompleteOption} opt * @returns {Promise } */ shouldComplete?(opt: CompleteOption): ProviderResult /** * Invoke completion * * @public * @param {CompleteOption} opt * @param {CancellationToken} token * @returns {Promise} */ doComplete(opt: CompleteOption, token: CancellationToken): ProviderResult /** * Action for complete item on complete item selected * * @public * @param {VimCompleteItem} item * @param {CancellationToken} token * @returns {Promise} */ onCompleteResolve?(item: VimCompleteItem, token: CancellationToken): ProviderResult /** * Action for complete item on complete done * * @public * @param {VimCompleteItem} item * @returns {Promise} */ onCompleteDone?(item: VimCompleteItem, opt: CompleteOption): ProviderResult shouldCommit?(item: VimCompleteItem, character: string): boolean } export namespace sources { /** * Names of registered sources. */ export const names: ReadonlyArray export const sources: ReadonlyArray /** * Check if source exists by name. */ export function has(name: string): boolean /** * Get source by name. */ export function getSource(name: string): ISource | null /** * Add source to sources list. * * Note: Use `sources.createSource()` to register new source is recommended for * user configuration support. */ export function addSource(source: ISource): Disposable /** * Create source by source config, configurations starts with `coc.source.{name}` * are automatically supported. * * `name` and `doComplete()` must be provided in config. */ export function createSource(config: SourceConfig): Disposable /** * Get list of all source stats. */ export function sourceStats(): SourceStat[] /** * Call refresh for _name_ source or all sources. */ export function refresh(name?: string): Promise /** * Toggle state of _name_ source. */ export function toggleSource(name: string): void /** * Remove source by name. */ export function removeSource(name: string): void } // }} // TreeView related {{ export interface TreeItemLabel { label: string highlights?: [number, number][] } export interface TreeItemIcon { text: string hlGroup: string } /** * Collapsible state of the tree item */ export enum TreeItemCollapsibleState { /** * Determines an item can be neither collapsed nor expanded. Implies it has no children. */ None = 0, /** * Determines an item is collapsed */ Collapsed = 1, /** * Determines an item is expanded */ Expanded = 2 } export class TreeItem { /** * A human-readable string describing this item. When `falsy`, it is derived from {@link TreeItem.resourceUri resourceUri}. */ label?: string | TreeItemLabel /** * Description rendered less prominently after label. */ description?: string /** * The icon path or {@link ThemeIcon} for the tree item. * When `falsy`, {@link ThemeIcon.Folder Folder Theme Icon} is assigned, if item is collapsible otherwise {@link ThemeIcon.File File Theme Icon}. * When a file or folder {@link ThemeIcon} is specified, icon is derived from the current file icon theme for the specified theme icon using {@link TreeItem.resourceUri resourceUri} (if provided). */ icon?: TreeItemIcon /** * Optional id for the tree item that has to be unique across tree. The id is used to preserve the selection and expansion state of the tree item. * * If not provided, an id is generated using the tree item's resourceUri when exists. **Note** that when labels change, ids will change and that selection and expansion state cannot be kept stable anymore. */ id?: string /** * The {@link Uri} of the resource representing this item. * * Will be used to derive the {@link TreeItem.label label}, when it is not provided. * Will be used to derive the icon from current file icon theme, when {@link TreeItem.iconPath iconPath} has {@link ThemeIcon} value. */ resourceUri?: Uri /** * The tooltip text when you hover over this item. */ tooltip?: string | MarkupContent /** * The {@link Command} that should be executed when the tree item is selected. * * Please use `vscode.open` or `vscode.diff` as command IDs when the tree item is opening * something in the editor. Using these commands ensures that the resulting editor will * appear consistent with how other built-in trees open editors. */ command?: Command /** * {@link TreeItemCollapsibleState} of the tree item. */ collapsibleState?: TreeItemCollapsibleState /** * @param label A human-readable string describing this item * @param collapsibleState {@link TreeItemCollapsibleState} of the tree item. Default is {@link TreeItemCollapsibleState.None} */ constructor(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState) /** * @param resourceUri The {@link Uri} of the resource representing this item. * @param collapsibleState {@link TreeItemCollapsibleState} of the tree item. Default is {@link TreeItemCollapsibleState.None} */ constructor(resourceUri: Uri, collapsibleState?: TreeItemCollapsibleState) } /** * Action resolved by {@link TreeDataProvider} */ export interface TreeItemAction { /** * Label text in menu. */ title: string handler: (item: T) => ProviderResult } /** * Options for creating a {@link TreeView} */ export interface TreeViewOptions { /** * bufhidden option for TreeView, default to 'wipe' */ bufhidden?: 'hide' | 'unload' | 'delete' | 'wipe' /** * Fixed width for window, default to true */ winfixwidth?: boolean /** * Enable filter feature, default to false */ enableFilter?: boolean /** * Disable indent of leaves without children, default to false */ disableLeafIndent?: boolean /** * A data provider that provides tree data. */ treeDataProvider: TreeDataProvider /** * Whether the tree supports multi-select. When the tree supports multi-select and a command is executed from the tree, * the first argument to the command is the tree item that the command was executed on and the second argument is an * array containing all selected tree items. */ canSelectMany?: boolean } /** * The event that is fired when an element in the {@link TreeView} is expanded or collapsed */ export interface TreeViewExpansionEvent { /** * Element that is expanded or collapsed. */ readonly element: T } /** * The event that is fired when there is a change in {@link TreeView.selection tree view's selection} */ export interface TreeViewSelectionChangeEvent { /** * Selected elements. */ readonly selection: T[] } /** * The event that is fired when there is a change in {@link TreeView.visible tree view's visibility} */ export interface TreeViewVisibilityChangeEvent { /** * `true` if the {@link TreeView tree view} is visible otherwise `false`. */ readonly visible: boolean } /** * Represents a Tree view */ export interface TreeView extends Disposable { /** * Event that is fired when an element is expanded */ readonly onDidExpandElement: Event> /** * Event that is fired when an element is collapsed */ readonly onDidCollapseElement: Event> /** * Currently selected elements. */ readonly selection: T[] /** * Event that is fired when the {@link TreeView.selection selection} has changed */ readonly onDidChangeSelection: Event> /** * Event that is fired when {@link TreeView.visible visibility} has changed */ readonly onDidChangeVisibility: Event /** * `true` if the {@link TreeView tree view} is visible otherwise `false`. * * **NOTE:** is `true` when TreeView visible on other tab. */ readonly visible: boolean /** * Window id used by TreeView. */ readonly windowId: number | undefined /** * An optional human-readable message that will be rendered in the view. * Setting the message to null, undefined, or empty string will remove the message from the view. */ message?: string /** * The tree view title is initially taken from viewId of TreeView * Changes to the title property will be properly reflected in the UI in the title of the view. */ title?: string /** * An optional human-readable description which is rendered less prominently in the title of the view. * Setting the title description to null, undefined, or empty string will remove the description from the view. */ description?: string /** * Reveals the given element in the tree view. * If the tree view is not visible then the tree view is shown and element is revealed. * * By default revealed element is selected. * In order to not to select, set the option `select` to `false`. * In order to focus, set the option `focus` to `true`. * In order to expand the revealed element, set the option `expand` to `true`. To expand recursively set `expand` to the number of levels to expand. * **NOTE:** You can expand only to 3 levels maximum. * * **NOTE:** The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ reveal(element: T, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable /** * Create tree view in new window. * * **NOTE:** TreeView with same viewId in current tab would be disposed. * * @param splitCommand The command to open TreeView window, default to 'belowright 30vs' * @return `true` if window shown. */ show(splitCommand?: string): Promise } /** * A data provider that provides tree data */ export interface TreeDataProvider { /** * An optional event to signal that an element or root has changed. * This will trigger the view to update the changed element/root and its children recursively (if shown). * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. */ onDidChangeTreeData?: Event /** * Get {@link TreeItem} representation of the `element` * * @param element The element for which {@link TreeItem} representation is asked for. * @return {@link TreeItem} representation of the element */ getTreeItem(element: T): TreeItem | Thenable /** * Get the children of `element` or root if no element is passed. * * @param element The element from which the provider gets children. Can be `undefined`. * @return Children of `element` or root if no element is passed. */ getChildren(element?: T): ProviderResult /** * Optional method to return the parent of `element`. * Return `null` or `undefined` if `element` is a child of root. * * **NOTE:** This method should be implemented in order to access {@link TreeView.reveal reveal} API. * * @param element The element for which the parent has to be returned. * @return Parent of `element`. */ getParent?(element: T): ProviderResult /** * Called on hover to resolve the {@link TreeItem.tooltip TreeItem} property if it is undefined. * Called on tree item click/open to resolve the {@link TreeItem.command TreeItem} property if it is undefined. * Only properties that were undefined can be resolved in `resolveTreeItem`. * Functionality may be expanded later to include being called to resolve other missing * properties on selection and/or on open. * * Will only ever be called once per TreeItem. * * onDidChangeTreeData should not be triggered from within resolveTreeItem. * * *Note* that this function is called when tree items are already showing in the UI. * Because of that, no property that changes the presentation (label, description, etc.) * can be changed. * * @param item Undefined properties of `item` should be set then `item` should be returned. * @param element The object associated with the TreeItem. * @param token A cancellation token. * @return The resolved tree item or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ resolveTreeItem?(item: TreeItem, element: T, token: CancellationToken): ProviderResult /** * Called with current element to resolve actions. * Called when user press 'actions' key. * * @param item Resolved item. * @param element The object under cursor. */ resolveActions?(item: TreeItem, element: T): ProviderResult[]> } // }} // workspace module {{ /** * An event describing the change in Configuration */ export interface ConfigurationChangeEvent { /** * Returns `true` if the given section for the given resource (if provided) is affected. * * @param section Configuration name, supports _dotted_ names. * @param resource A resource URI. * @return `true` if the given section for the given resource (if provided) is affected. */ affectsConfiguration(section: string, scope?: ConfigurationScope): boolean } export interface WillSaveEvent extends TextDocumentWillSaveEvent { /** * Allows to pause the event loop and to apply [pre-save-edits](#TextEdit). * Edits of subsequent calls to this function will be applied in order. The * edits will be *ignored* if concurrent modifications of the document happened. * * *Note:* This function can only be called during event dispatch and not * in an asynchronous manner: * * ```ts * workspace.onWillSaveTextDocument(event => { * // async, will *throw* an error * setTimeout(() => event.waitUntil(promise)); * * // sync, OK * event.waitUntil(promise); * }) * ``` * * @param thenable A thenable that resolves to [pre-save-edits](#TextEdit). */ waitUntil(thenable: Thenable): void } export interface KeymapOption { /** * Use as rhs command prefix, ignored on insert mode ( is used on insert mode), see `:h map-cmd`. */ cmd?: boolean /** * When invoke the callback, send request to NodeJS instead of notification, default `true`. */ sync?: boolean /** * Cancel completion before invoke callback, default `true`, insert mode only. */ cancel?: boolean /** * Use for keymap, default `true`. */ silent?: boolean /** * Enable repeat support for repeat.vim, default `false`. */ repeat?: boolean /** * Use map argument, see `:h :map-special`, vim9 only. */ special?: boolean } export interface DidChangeTextDocumentParams { /** * The document that did change. The version number points * to the version after all provided content changes have * been applied. */ readonly textDocument: { version: number uri: string } /** * The affected document. */ readonly document: LinesTextDocument /** * The actual content changes. The content changes describe single state changes * to the document. So if there are two content changes c1 (at array index 0) and * c2 (at array index 1) for a document in state S then c1 moves the document from * S to S' and c2 from S' to S''. So c1 is computed on the state S and c2 is computed * on the state S'. */ readonly contentChanges: ReadonlyArray /** * Buffer number of document. */ readonly bufnr: number /** * Original content before change */ readonly original: string /** * Original lines before change */ readonly originalLines: ReadonlyArray } export interface EditerState { document: LinesTextDocument position: Position } export type MapMode = 'n' | 'i' | 'v' | 'x' | 's' | 'o' | '!' | 't' | 'c' | 'l' export interface Autocmd { /** * Vim event or event set. */ event: string | string[] /** * Callback functions that called with evaled arglist as arguments. */ callback: (...args: any[]) => void | Promise /** * Match pattern, default to `*`. */ pattern?: string | string[] /** * Vim expression that eval to arguments of callback, default to `[]` */ arglist?: string[] /** * buffer number for buffer-local autocommand. */ buffer?: number /** * the command is executed once when `true`, see `:h autocmd-once` */ once?: boolean /** * allow nested autocmd when `true`, see `:h autocmd-nested` */ nested?: boolean /** * Use request when `true`, use notification by default. */ request?: boolean /** * `this` of callback. */ thisArg?: any } export interface Env { /** * |runtimepath| option of (neo)vim. */ readonly runtimepath: string /** * |virtualText| support in (neo)vim */ readonly virtualText: boolean /** * |guicursor| option of (neo)vim */ readonly guicursor: string /** * Could use float window on neovim, always false on vim. */ readonly floating: boolean /** * |sign_place()| and |sign_unplace()| can be used when true. */ readonly sign: boolean /** * Root directory of extensions. */ readonly extensionRoot: string /** * Process id of (neo)vim. */ readonly pid: number /** * Total columns of screen. */ readonly columns: number /** * Total lines of screen. */ readonly lines: number /** * Is true when |CompleteChanged| event is supported. */ readonly pumevent: boolean /** * |cmdheight| option of (neo)vim. */ readonly cmdheight: number /** * Value of |g:coc_filetype_map| */ readonly filetypeMap: { [index: string]: string } /** * Is true when not using neovim. */ readonly isVim: boolean /** * Is cygvim when true. */ readonly isCygwin: boolean /** * Is macvim when true. */ readonly isMacvim: boolean /** * Is true when iTerm.app is used on mac. */ readonly isiTerm: boolean /** * version of (neo)vim, on vim it's like: 8020750, on neoivm it's like */ readonly version: string /** * |v:progpath| value, could be empty. */ readonly progpath: string /** * Is true when dialog feature is supported */ readonly dialog: boolean /** * Is true when terminal feature is supported */ readonly terminal: boolean /** * Is true when vim's textprop is supported. */ readonly textprop: boolean } /** * Store & retrieve most recent used items. */ export interface Mru { /** * Load iems from mru file */ load(): Promise /** * Add item to mru file. */ add(item: string): Promise /** * Remove item from mru file. */ remove(item: string): Promise /** * Remove the data file. */ clean(): Promise } /** * Option to create task that runs in (neo)vim. */ export interface TaskOptions { /** * The command to run, without arguments */ cmd: string /** * Arguments of command. */ args?: string[] /** * Current working directory of the task, Default to current vim's cwd. */ cwd?: string /** * Additional environment key-value pairs. */ env?: { [key: string]: string } /** * Use pty when true. */ pty?: boolean /** * Detach child process when true. */ detach?: boolean } /** * Controls long running task started by (neo)vim. * Useful to keep the task running after CocRestart. */ export interface Task extends Disposable { /** * Fired on task exit with exit code. */ onExit: Event /** * Fired with lines on stdout received. */ onStdout: Event /** * Fired with lines on stderr received. */ onStderr: Event /** * Start task, task will be restarted when already running. * * @param {TaskOptions} opts * @returns {Promise} */ start(opts: TaskOptions): Promise /** * Stop task by SIGTERM or SIGKILL */ stop(): Promise /** * Check if the task is running. */ running: Promise } /** * A simple json database. */ export interface JsonDB { filepath: string /** * Get data by key. * * @param {string} key unique key allows dot notation. * @returns {any} */ fetch(key: string): any /** * Check if key exists * * @param {string} key unique key allows dot notation. */ exists(key: string): boolean /** * Delete data by key * * @param {string} key unique key allows dot notation. */ delete(key: string): void /** * Save data with key */ push(key: string, data: number | null | boolean | string | { [index: string]: any }): void /** * Empty db file. */ clear(): void /** * Remove db file. */ destroy(): void } export interface RenameEvent { oldUri: Uri newUri: Uri } export interface FileSystemWatcher { readonly ignoreCreateEvents: boolean readonly ignoreChangeEvents: boolean readonly ignoreDeleteEvents: boolean readonly onDidCreate: Event readonly onDidChange: Event readonly onDidDelete: Event readonly onDidRename: Event dispose(): void } export type ConfigurationScope = string | null | Uri | TextDocument | WorkspaceFolder | { uri?: string; languageId?: string } export interface ConfigurationInspect { key: string defaultValue?: T globalValue?: T workspaceValue?: T workspaceFolderValue?: T } export enum ConfigurationTarget { Global = 1, /** * Not exists with coc.nvim yet. */ Workspace = 2, WorkspaceFolder = 3 } export interface WorkspaceConfiguration { /** * Return a value from this configuration. * * @param section Configuration name, supports _dotted_ names. * @return The value `section` denotes or `undefined`. */ get(section: string): T | undefined /** * Return a value from this configuration. * * @param section Configuration name, supports _dotted_ names. * @param defaultValue A value should be returned when no value could be found, is `undefined`. * @return The value `section` denotes or the default. */ get(section: string, defaultValue: T): T /** * Check if this configuration has a certain value. * * @param section Configuration name, supports _dotted_ names. * @return `true` if the section doesn't resolve to `undefined`. */ has(section: string): boolean /** * Retrieve all information about a configuration setting. A configuration value * often consists of a *default* value, a global or installation-wide value, * a workspace-specific value * * *Note:* The configuration name must denote a leaf in the configuration tree * (`editor.fontSize` vs `editor`) otherwise no result is returned. * * @param section Configuration name, supports _dotted_ names. * @return Information about a configuration setting or `undefined`. */ inspect(section: string): ConfigurationInspect | undefined /** * Update a configuration value. * The updated configuration values are persisted to configuration file. * * @param section Configuration name, supports _dotted_ names. * @param value The new value. * @param updateTarget Target of configuration, use global user configuration when is `true`, * when `false` or undefined use workspace folder confirmation. */ update(section: string, value: any, updateTarget?: ConfigurationTarget | boolean): Thenable /** * Readable dictionary that backs this configuration. */ readonly [key: string]: any } export interface BufferSyncItem { /** * Called on buffer unload. */ dispose: () => void /** * Called on buffer content change. */ onChange?(e: DidChangeTextDocumentParams): void /** * Called when NodeJS client receive lines change event, could be before or * after `TextChangedI` and `TextChangedP` events, but always before * `TextDocumentContentChange` event. */ onTextChange?(): void /** * Called on `WindowVisible` event when exists. * `region` contains, 1 based, end inclusive topline, botline */ onVisible?(winid: number, region: Readonly<[number, number]>): void } export interface BufferSync { /** * Current items. */ readonly items: Iterable /** * Get created item by uri */ getItem(uri: string): T | undefined /** * Get created item by bufnr */ getItem(bufnr: number): T | undefined dispose: () => void } export interface FuzzyMatchResult { score: number, positions: Uint32Array } export interface FuzzyMatchHighlights { score: number highlights: AnsiHighlight[] } /** * An array representing a fuzzy match. * * 0. the score * 1. the offset at which matching started * 2. `` * 3. `` * 4. `` etc */ export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]] export interface FuzzyScoreOptions { readonly boostFullMatch: boolean /** * Allows first match to be a weak match */ readonly firstMatchCanBeWeak: boolean } /** * Match kinds could be: * * - 'aggressive' with fixed match for permutations. * - 'any' fast match with first 13 characters only. * - 'normal' nothing special. */ export type FuzzyKind = 'normal' | 'aggressive' | 'any' export type ScoreFunction = (word: string, wordPos?: number) => FuzzyScore | undefined export interface FuzzyMatch { /** * Create 0 index byte spans from matched text and FuzzyScore. * Mostly used for create {@link HighlightItem highlight items}. * * @param {string} text The matched text for count bytes. * @param {FuzzyScore} score * @returns {Iterable<[number, number]>} */ matchScoreSpans(text: string, score: FuzzyScore): Iterable<[number, number]> /** * * Create a score function * * @param {string} pattern The pattern to match. * @param {number} patternPos Start character index of pattern. * @param {FuzzyScoreOptions} options Optional option. * @param {FuzzyKind} kind Use 'normal' when undefined. * @returns {ScoreFunction} */ createScoreFunction(pattern: string, patternPos: number, options?: FuzzyScoreOptions, kind?: FuzzyKind): ScoreFunction /** * Initialize match by set the match pattern and matchSeq. * * @param {string} pattern The match pattern Limited length to 256. * @param {boolean} matchSeq Match the sequence characters instead of split pattern by white spaces. * @returns {void} */ setPattern(pattern: string, matchSeq?: boolean): void /** * Get the match result of text including score and character index * positions, return undefined when no match found. * * @param {string} text Content to match * @returns {FuzzyMatchResult | undefined} */ match(text: string): FuzzyMatchResult | undefined /** * Match character positions to column spans. * Better than matchHighlights method by reduce iteration. * * @param {string} text Context to match * @param {ArrayLike} positions Matched character index positions. * @param {number} max Maximum column number to calculate * @returns {Iterable<[number, number]>} */ matchSpans(text: string, positions: ArrayLike, max?: number): Iterable<[number, number]> /** * Get the match highlights result, including score and highlight items. * Return undefined when no match found. * * @param {string} text Content to match * @returns {FuzzyMatchHighlights | undefined} */ matchHighlights(text: string, hlGroup: string): FuzzyMatchHighlights | undefined } export interface TextDocumentMatch { readonly uri: string readonly languageId: string } export namespace workspace { export const nvim: Neovim export const isTrusted = true /** * Current buffer number, could be wrong since vim could not send autocmd as expected. * * @deprecated will be removed in the feature. */ export const bufnr: number /** * Current document. */ export const document: Promise /** * Environments or current (neo)vim. */ export const env: Env /** * Float window or popup can work. */ export const floatSupported: boolean /** * Current working directory of vim. */ export const cwd: string /** * Current workspace root. */ export const root: string /** * @deprecated aliased to root. */ export const rootPath: string /** * Not neovim when true. */ export const isVim: boolean /** * Is neovim when true. */ export const isNvim: boolean /** * All filetypes of loaded documents. */ export const filetypes: ReadonlySet /** * All languageIds of loaded documents. */ export const languageIds: ReadonlySet /** * Root directory of coc.nvim */ export const pluginRoot: string /** * Exists channel names. */ export const channelNames: ReadonlyArray /** * Loaded documents that attached. */ export const documents: ReadonlyArray /** * Current document array. */ export const textDocuments: ReadonlyArray /** * Current workspace folders. */ export const workspaceFolders: ReadonlyArray /** * Directory paths of workspaceFolders. */ export const folderPaths: ReadonlyArray /** * Current workspace folder, could be null when vim started from user's home. * * @deprecated */ export const workspaceFolder: WorkspaceFolder | null export const onDidCreateFiles: Event export const onDidRenameFiles: Event export const onDidDeleteFiles: Event export const onWillCreateFiles: Event export const onWillRenameFiles: Event export const onWillDeleteFiles: Event /** * Event fired on workspace folder change. */ export const onDidChangeWorkspaceFolders: Event /** * Event fired after document create. */ export const onDidOpenTextDocument: Event /** * Event fired after document unload. */ export const onDidCloseTextDocument: Event /** * Event fired on document change. */ export const onDidChangeTextDocument: Event /** * Event fired before document save. */ export const onWillSaveTextDocument: Event /** * Event fired after document save. */ export const onDidSaveTextDocument: Event /** * Event fired on configuration change. Configuration change could by many * reasons, including: * * - Changes detected from `coc-settings.json`. * - Change to document that using another configuration file. * - Configuration change by call update API of WorkspaceConfiguration. */ export const onDidChangeConfiguration: Event /** * Fired when vim's runtimepath change detected. */ export const onDidRuntimePathChange: Event> /** * Returns a path that is relative to the workspace folder or folders. * * When there are no {@link workspace.workspaceFolders workspace folders} or when the path * is not contained in them, the input is returned. * * @param pathOrUri A path or uri. When a uri is given its {@link Uri.fsPath fsPath} is used. * @param includeWorkspaceFolder When `true` and when the given path is contained inside a * workspace folder the name of the workspace is prepended. Defaults to `true` when there are * multiple workspace folders and `false` otherwise. * @return A path relative to the root or the input. */ export function asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string /** * Returns converted unix path when the vim is built with win32unix enabled. Original fullpath is returned when the * convert is not necessary. Only needed when the fullpath is passed vim directly. * * @param fullpath The filepath to fix, only windows absolute filepath is fixed. */ export function fixWin32unixFilepath(fullpath: string): string /** * Opens a document. Will return early if this document is already open. Otherwise * the document is loaded and the {@link workspace.onDidOpenTextDocument didOpen}-event fires. * * The document is denoted by an {@link Uri}. Depending on the {@link Uri.scheme scheme} the * following rules apply: * * `file`-scheme: Open a file on disk (`openTextDocument(Uri.file(path))`). Will be rejected if the file * does not exist or cannot be loaded. * * `untitled`-scheme: Open a blank untitled file with associated path (`openTextDocument(Uri.file(path).with({ scheme: 'untitled' }))`). * The language will be derived from the file name. * * For all other schemes contributed {@link TextDocumentContentProvider text document content providers} and * {@link FileSystemProvider file system providers} are consulted. * * *Note* that the lifecycle of the returned document is owned by the editor and not by the extension. That means an * {@linkcode workspace.onDidCloseTextDocument onDidClose}-event can occur at any time after opening it. * * @param uri Identifies the resource to open. * @return A promise that resolves to a {@link Document document}. */ export function openTextDocument(uri: Uri): Thenable /** * A short-hand for `openTextDocument(Uri.file(fileName))`. * * @see {@link openTextDocument} * @param fileName A name of a file on disk. * @return A promise that resolves to a {@link Document document}. */ export function openTextDocument(fileName: string): Thenable /** * Get display cell count of text on vim. * Control character below 0x80 are considered as 1. * * @param text Text to display. * @return The cells count. */ export function getDisplayWidth(text: string, cache?: boolean): number /** * Like vim's has(), but for version check only. * Check patch on neovim and check nvim on vim would return false. * * For example: * - has('nvim-0.6.0') * - has('patch-7.4.248') */ export function has(feature: string): boolean /** * Register autocmd on vim. * * Note: avoid request autocmd when possible since vim could be blocked * forever when request triggered during request. */ export function registerAutocmd(autocmd: Autocmd, disposables?: Disposable[]): Disposable /** * Watch for vim's global option change. */ export function watchOption(key: string, callback: (oldValue: any, newValue: any) => Thenable | void, disposables?: Disposable[]): void /** * Watch for vim's global variable change, works on neovim only. */ export function watchGlobal(key: string, callback?: (oldValue: any, newValue: any) => Thenable | void, disposables?: Disposable[]): void /** * Check if selector match document. */ export function match(selector: DocumentSelector, document: TextDocumentMatch): number /** * Findup from filename or filenames from current filepath or root. * * @return fullpath of file or null when not found. */ export function findUp(filename: string | string[]): Promise /** * Get possible watchman binary path. */ export function getWatchmanPath(): string | null /** * Get configuration by section and optional resource uri. */ export function getConfiguration(section?: string, scope?: ConfigurationScope): WorkspaceConfiguration /** * Resolve internal json schema, uri should starts with `vscode://` */ export function resolveJSONSchema(uri: string): any /** * Get created document by uri or bufnr. */ export function getDocument(uri: number | string): Document | null | undefined /** * Apply WorkspaceEdit. */ export function applyEdit(edit: WorkspaceEdit, metadata?: WorkspaceEditMetadata): Promise /** * Convert location to quickfix item. */ export function getQuickfixItem(loc: Location | LocationLink, text?: string, type?: string, module?: string): Promise /** * Convert locations to quickfix list. */ export function getQuickfixList(locations: Location[]): Promise> /** * Populate locations to UI. */ export function showLocations(locations: Location[]): Promise /** * Get content of line by uri and line. */ export function getLine(uri: string, line: number): Promise /** * Get WorkspaceFolder of uri */ export function getWorkspaceFolder(uri: string | Uri): WorkspaceFolder | undefined /** * Get content from buffer or file by uri. */ export function readFile(uri: string): Promise /** * Get current document and position. */ export function getCurrentState(): Promise /** * Get format options of uri or current buffer. */ export function getFormatOptions(uri?: string): Promise /** * Jump to location. */ export function jumpTo(uri: string | Uri, position?: Position | null, openCommand?: string): Promise /** * Create a file in vim and disk */ export function createFile(filepath: string, opts?: CreateFileOptions): Promise /** * Load uri as document, buffer would be invisible if not loaded. */ export function loadFile(uri: string): Promise /** * Load the files that not loaded */ export function loadFiles(uris: string[]): Promise /** * Rename file in vim and disk */ export function renameFile(oldPath: string, newPath: string, opts?: RenameFileOptions): Promise /** * Delete file from vim and disk. */ export function deleteFile(filepath: string, opts?: DeleteFileOptions): Promise /** * Open resource by uri */ export function openResource(uri: string): Promise /** * Resolve full path of module from yarn or npm global directory. */ export function resolveModule(name: string): Promise /** * Run nodejs command */ export function runCommand(cmd: string, cwd?: string, timeout?: number): Promise /** * Expand filepath with `~` and/or environment placeholders */ export function expand(filepath: string): string /** * Call a function by use notifications, useful for functions like |input| that could block vim. */ export function callAsync(method: string, args: any[]): Promise /** * Register TextDocumentContentProvider for custom scheme */ export function registerTextDocumentContentProvider(scheme: string, provider: TextDocumentContentProvider): Disposable /** * Register unique global key-mapping with `(coc-{key})` as lhs. * 'noremap' is always used, Throw error when {key} already exists. * * @param {MapMode[]} modes - Array of map mode short-name. * @param {string} key - Unique name, should only use alphabetical characters and '-'. * @param {Function} fn - Callback function. * @param {KeymapOption} opts - Optional option. * @returns {Disposable} */ export function registerKeymap(modes: MapMode[], key: string, fn: () => ProviderResult, opts?: KeymapOption): Disposable /** * Register expr mapping global or local to buffer. * * Unlike :map, space in {lhs} is accepted as part of the {lhs}, keycodes * are replaced are usual. * 'noremap' and map arguments , are always used. * * @param {MapMode} mode - Mode short-name. * @param {string} rhs - rhs of key-mapping. * @param {Function} fn - callback function. * @param {number | boolean} buffer - Buffer number or current buffer by use `true` or 0, default to false. * @param {boolean} cancel - Cancel pupop menu before invoke callback, insert mode only, define to true. * @returns {Disposable} */ export function registerExprKeymap(mode: MapMode, rhs: string, fn: () => ProviderResult, buffer?: number | boolean, cancel?: boolean): Disposable /** * Register local keymap with callback. * * Unlike :map, space in {lhs} is accepted as part of the {lhs}, keycodes * are replaced are usual. * 'noremap' and map arguments are always used. * * @param {number} bufnr - buffer number, use 0 for current buffer. * @param {'n' | 'i' | 'v' | 's' | 'x'} mode - mode short-name. * @param {string} lhs - lhs of key-mapping. * @param {Function} fn - callback function. * @param {KeymapOption | boolean} opts - Optional option, when it's boolean value, indicate use notification or not. * @returns {Disposable} */ export function registerLocalKeymap(bufnr: number, mode: 'n' | 'i' | 'v' | 's' | 'x', lhs: string, fn: () => ProviderResult, opts?: KeymapOption | boolean): Disposable /** * Register for buffer sync objects, created sync object should be * disposable and provide optional event handlers: * * - `onChange` called on `onDidChangeTextDocument` event. * - `onTextChange` called on line change event from vim. * - `onVisible` called on `WindowVisible` event. * * The document is always attached and not command line buffer. * * @param create Called for each attached document and on document create. * @returns Disposable */ export function registerBufferSync(create: (doc: Document) => T | undefined): BufferSync /** * Create a FuzzyMatch instance using wasm module. * The FuzzyMatch does the same match algorithm as vim's `:h matchfuzzypos()` */ export function createFuzzyMatch(): FuzzyMatch /** * Compute word ranges of opened document in specified range. * * @param {string | number} uri Uri of resource * @param {Range} range Range of resource * @param {CancellationToken} token * @returns {Promise<{ [word: string]: Range[] } | null>} */ export function computeWordRanges(uri: string | number, range: Range, token?: CancellationToken): Promise<{ [word: string]: Range[] } | null> /** * Create a FileSystemWatcher instance, when watchman doesn't exist, the * returned FileSystemWatcher can still be used, but not work at all. */ export function createFileSystemWatcher(globPattern: GlobPattern, ignoreCreate?: boolean, ignoreChange?: boolean, ignoreDelete?: boolean): FileSystemWatcher /** * Find files across all {@link workspace.workspaceFolders workspace folders} in the workspace. * * @example * findFiles('**​/*.js', '**​/node_modules/**', 10) * * @param include A {@link GlobPattern glob pattern} that defines the files to search for. The glob pattern * will be matched against the file paths of resulting matches relative to their workspace. * Use a {@link RelativePattern relative pattern} to restrict the search results to a {@link WorkspaceFolder workspace folder}. * @param exclude A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` or`null`, * no excludes will apply. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no * {@link workspace.workspaceFolders workspace folders} are opened. */ export function findFiles(include: GlobPattern, exclude?: GlobPattern | null, maxResults?: number, token?: CancellationToken): Thenable /** * Create persistence Mru instance. */ export function createMru(name: string): Mru /** * Create Task instance that runs in (neo)vim, no shell. * * @param id Unique id string, like `TSC` */ export function createTask(id: string): Task /** * Create DB instance at extension root. */ export function createDatabase(name: string): JsonDB } // }} // window module {{ /** * Represents how a terminal exited. */ export interface TerminalExitStatus { /** * The exit code that a terminal exited with, it can have the following values: * - Zero: the terminal process or custom execution succeeded. * - Non-zero: the terminal process or custom execution failed. * - `undefined`: the user forcibly closed the terminal or a custom execution exited * without providing an exit code. */ readonly code: number | undefined } export interface TerminalOptions { /** * A human-readable string which will be used to represent the terminal in the UI. */ name?: string /** * A path to a custom shell executable to be used in the terminal. */ shellPath?: string /** * Args for the custom shell executable, this does not work on Windows (see #8429) */ shellArgs?: string[] /** * A path or URI for the current working directory to be used for the terminal. */ cwd?: string /** * Object with environment variables that will be added to the VS Code process. */ env?: { [key: string]: string | null } /** * Whether the terminal process environment should be exactly as provided in * `TerminalOptions.env`. When this is false (default), the environment will be based on the * window's environment and also apply configured platform settings like * `terminal.integrated.windows.env` on top. When this is true, the complete environment * must be provided as nothing will be inherited from the process or any configuration. * Neovim only. */ strictEnv?: boolean } /** * An individual terminal instance within the integrated terminal. */ export interface Terminal { /** * The bufnr of terminal buffer. */ readonly bufnr: number /** * The name of the terminal. */ readonly name: string /** * The process ID of the shell process. */ readonly processId: Promise /** * The exit status of the terminal, this will be undefined while the terminal is active. * * **Example:** Show a notification with the exit code when the terminal exists with a * non-zero exit code. * ```typescript * window.onDidCloseTerminal(t => { * if (t.exitStatus && t.exitStatus.code) { * vscode.window.showInformationMessage(`Exit code: ${t.exitStatus.code}`); * } * }); * ``` */ readonly exitStatus: TerminalExitStatus | undefined /** * Send text to the terminal. The text is written to the stdin of the underlying pty process * (shell) of the terminal. * * @param text The text to send. * @param addNewLine Whether to add a new line to the text being sent, this is normally * required to run a command in the terminal. The character(s) added are \n or \r\n * depending on the platform. This defaults to `true`. */ sendText(text: string, addNewLine?: boolean): void /** * Show the terminal panel and reveal this terminal in the UI, return false when failed. * * @param preserveFocus When `true` the terminal will not take focus. */ show(preserveFocus?: boolean): Promise /** * Hide the terminal panel if this terminal is currently showing. */ hide(): void /** * Dispose and free associated resources. */ dispose(): void } /** * Option for create status item. */ export interface StatusItemOption { progress?: boolean } /** * Status item that included in `g:coc_status` */ export interface StatusBarItem { /** * The priority of this item. Higher value means the item should * be shown more to the left. */ readonly priority: number isProgress: boolean /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * * `My text $(icon-name) contains icons like $(icon-name) this one.` * * Where the icon-name is taken from the [octicon](https://octicons.github.com) icon set, e.g. * `light-bulb`, `thumbsup`, `zap` etc. */ text: string /** * Shows the entry in the status bar. */ show(): void /** * Hide the entry in the status bar. */ hide(): void /** * Dispose and free associated resources. Call * [hide](#StatusBarItem.hide). */ dispose(): void } /** * Value-object describing where and how progress should show. */ export interface ProgressOptions { /** * A human-readable string which will be used to describe the * operation. */ title?: string /** * Controls if a cancel button should show to allow the user to * cancel the long running operation. */ cancellable?: boolean } /** * Defines a generalized way of reporting progress updates. */ export interface Progress { /** * Report a progress update. * * @param value A progress item, like a message and/or an * report on how much work finished */ report(value: T): void } /** * Represents an action that is shown with an information, warning, or * error message. * * @see [showInformationMessage](#window.showInformationMessage) * @see [showWarningMessage](#window.showWarningMessage) * @see [showErrorMessage](#window.showErrorMessage) */ export interface MessageItem { /** * A short title like 'Retry', 'Open Log' etc. */ title: string /** * A hint for modal dialogs that the item should be triggered * when the user cancels the dialog (e.g. by pressing the ESC * key). * * Note: this option is ignored for non-modal messages. * Note: not used by coc.nvim for now. */ isCloseAffordance?: boolean } export interface DialogButton { /** * Use by callback, should >= 0 */ index: number text: string /** * Not shown when true */ disabled?: boolean } export interface DialogConfig { /** * Content shown in window. */ content: string /** * Optional title text. */ title?: string /** * show close button, default to true when not specified. */ close?: boolean /** * highlight group for dialog window, default to `"dialog.floatHighlight"` or 'CocFlating' */ highlight?: string /** * highlight items of content. */ highlights?: ReadonlyArray /** * highlight groups for border, default to `"dialog.borderhighlight"` or 'CocFlating' */ borderhighlight?: string /** * Buttons as bottom of dialog. */ buttons?: DialogButton[] /** * index is -1 for window close without button click */ callback?: (index: number) => void } export type NotificationKind = 'error' | 'info' | 'warning' | 'progress' export interface NotificationConfig { kind?: NotificationKind content?: string /** * Optional title text. */ title?: string /** * Buttons as bottom of dialog. */ buttons?: DialogButton[] /** * index is -1 for window close without button click */ callback?: (index: number) => void } /** * Options to configure the behavior of the quick pick UI. */ export interface QuickPickOptions { /** * An optional string that represents the title of the quick pick. */ title?: string /** * Placeholder text that shown when input value is empty. */ placeHolder?: string /** * An optional flag to include the description when filtering the picks. */ matchOnDescription?: boolean /** * An optional flag to make the picker accept multiple selections, if true the result is an array of picks. */ canPickMany?: boolean /** * @deprecated use placeHolder instead. */ placeholder?: string } /** * Represents an item that can be selected from * a list of items. */ export interface QuickPickItem { /** * A human-readable string which is rendered prominent */ label: string /** * A human-readable string which is rendered less prominent in the same line */ description?: string /** * Optional flag indicating if this item is picked initially. */ picked?: boolean } export interface QuickPickConfig { /** * An optional title. */ title?: string /** * Placeholder text that shown when input value is empty. */ placeholder?: string /** * Items to pick from. */ items: readonly T[] /** * Initial value of the filter text. */ value?: string /** * If multiple items can be selected at the same time. Defaults to false. */ canSelectMany?: boolean /** * If the filter text should also be matched against the description of the items. Defaults to false. */ matchOnDescription: boolean } export interface QuickPick { /** * Set or get current input value. */ value: string /** * An optional title. */ title: string | undefined /** * An optional placeholder text. */ placeholder: string | undefined /** * If the UI should show a progress indicator. Defaults to false. * * Change this to true, e.g., while loading more data or validating * user input. */ loading: boolean /** * Items to pick from. This can be read and updated by the extension. */ items: readonly T[] /** * Active items. This can be read and updated by the extension. */ activeItems: readonly T[] /** * If the filter text should also be matched against the description of the items. Defaults to false. */ matchOnDescription: boolean /** * If multiple items can be selected at the same time. Defaults to false. */ canSelectMany: boolean /** * Max height of list, `` */ maxHeight: number /** * Width of window, limited by `dialog.maxWidth` configuration and vim's 'columns'. * Undefined by default, which means the width is dynamically calculated. */ width: number | undefined /** * Represents the input prompt box field of the quickpick element **/ readonly inputBox: InputBox | undefined /** * The current selection index, can be used to act on an item with onDidFinish, even * if the item is not selected. The index corresponds to the .items or .activeItems * arrays, and can be used to index into them **/ readonly currIndex: number /** * The buffer for the popup element of the quick pick containing the .items to be selected **/ readonly buffer: number /** * The window for the popup element of the quick pick containing the .items to be selected **/ readonly winid: number | undefined /** * An event signaling when QuickPick closed, fired with selected items or null when canceled. */ readonly onDidFinish: Event /** * An event signaling when the value of the filter text has changed. */ readonly onDidChangeValue: Event /** * An event signaling when the selected items have changed. */ readonly onDidChangeSelection: Event /** * Makes the input UI visible in its current configuration. */ show(): Promise } export interface ScreenPosition { row: number col: number } export type MsgTypes = 'error' | 'warning' | 'more' export interface OpenTerminalOption { /** * Cwd of terminal, default to result of |getcwd()| */ cwd?: string /** * Close terminal on job finish, default to true. */ autoclose?: boolean /** * Keep focus current window, default to false. */ keepfocus?: boolean /** * Position of terminal window, default to 'right'. */ position?: 'bottom' | 'right' } /** * An output channel is a container for readonly textual information. * * To get an instance of an `OutputChannel` use * [createOutputChannel](#window.createOutputChannel). */ export interface OutputChannel { /** * The human-readable name of this output channel. */ readonly name: string readonly content: string /** * Append the given value to the channel. * * @param value A string, falsy values will not be printed. */ append(value: string): void /** * Append the given value and a line feed character * to the channel. * * @param value A string, falsy values will be printed. */ appendLine(value: string): void /** * Removes output from the channel. Latest `keep` lines will be remained. */ clear(keep?: number): void /** * Reveal this channel in the UI. * * @param preserveFocus When `true` the channel will not take focus. */ show(preserveFocus?: boolean): void /** * Hide this channel from the UI. */ hide(): void /** * Dispose and free associated resources. */ dispose(): void } export interface TerminalResult { bufnr: number success: boolean content?: string } export interface Dialog { /** * Buffer number of dialog. */ bufnr: number /** * Window id of dialog. */ winid: Promise dispose: () => void } export type HighlightItemDef = [string, number, number, number, number?, number?, number?] export interface HighlightDiff { remove: number[] removeMarkers: number[] add: HighlightItemDef[] } export interface MenuItem { text: string disabled?: boolean | { reason: string } } export interface MenuOption { /** * Title in menu window. */ title?: string, /** * Content in menu window as normal text. */ content?: string /** * Create and highlight shortcut characters. */ shortcuts?: boolean /** * Position of menu, default to 'cursor' */ position?: 'center' | 'cursor' } export interface InputOptions { /** * Placeholder text that shown when input value is empty. */ placeholder?: string /** * Position to show input, default to 'cursor' */ position?: 'cursor' | 'center' /** * Margin top editor when position is 'center' */ marginTop?: number /** * Border highlight of float window/popup, configuration `dialog.borderhighlight` used as default. */ borderhighlight?: string /** * Create key-mappings for quickpick list. */ list?: boolean } export interface InputPreference extends InputOptions { /** * Top, right, bottom, left border existence, default to [1,1,1,1] */ border?: [0 | 1, 0 | 1, 0 | 1, 0 | 1] /** * Rounded border, default to true, configuration `dialog.rounded` used as default. */ rounded?: boolean /** * Minimal window width, `g:coc_prompt_win_width` or 32 used as default. */ minWidth?: number /** * Maximum window width, configuration `dialog.maxWidth` used as default. */ maxWidth?: number } export interface InputDimension { readonly width: number readonly height: number /** * 0 based screen row */ readonly row: number /** * O based screen col */ readonly col: number } export interface InputBox { /** * Current input text, could be changed. */ value: string /** * Change or get title of input box. */ title: string /** * Change or get loading state of input box. */ loading: boolean /** * Change or get borderhighlight of input box. */ borderhighlight: string /** * Dimension of float window/popup */ readonly dimension: InputDimension /** * Buffer number of float window/popup */ readonly bufnr: number /** * An event signaling when the value has changed. */ readonly onDidChange: Event /** * An event signaling input finished, emit input value or null when canceled. */ readonly onDidFinish: Event } /** * FloatWinConfig. */ export interface FloatWinConfig { border?: boolean | [number, number, number, number] rounded?: boolean highlight?: string title?: string borderhighlight?: string close?: boolean maxHeight?: number maxWidth?: number winblend?: number focusable?: boolean shadow?: boolean preferTop?: boolean autoHide?: boolean offsetX?: number cursorline?: boolean modes?: string[] excludeImages?: boolean position?: "fixed" | "auto" top?: number bottom?: number left?: number right?: number } export interface FloatFactory { /** * Show documentations in float window/popup. * Window and buffer are reused when possible. * * @param docs List of documentations. * @param options Configuration for floating window/popup. */ show: (docs: Documentation[], options?: FloatWinConfig) => Promise /** * Close the float window created by this float factory. */ close: () => void /** * Check if float window is shown. */ activated: () => Promise /** * Unbind events */ dispose: () => void } export namespace window { /** * The currently active editor or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed * input most recently. */ export const activeTextEditor: TextEditor | undefined /** * The currently visible editors or an empty array. */ export const visibleTextEditors: readonly TextEditor[] /** * An {@link Event} which fires when the {@link window.activeTextEditor active editor} * has changed. *Note* that the event also fires when the active editor changes * to `undefined`. */ export const onDidChangeActiveTextEditor: Event /** * An {@link Event} which fires when the array of {@link window.visibleTextEditors visible editors} * has changed. */ export const onDidChangeVisibleTextEditors: Event /** * The currently opened terminals or an empty array. * onDidChangeTerminalState doesn't exist since we can't detect window resize on vim. */ export const terminals: readonly Terminal[] /** * Event fired after terminal created, only fired with Terminal that * created by `window.createTerminal` */ export const onDidOpenTerminal: Event /** * Event fired on terminal close, only fired with Terminal that created by * `window.createTerminal` */ export const onDidCloseTerminal: Event /** * Creates a {@link Terminal} with a backing shell process. * The terminal is created by (neo)vim. * * @param options A TerminalOptions object describing the characteristics of the new terminal. * @return A new Terminal. * @throws When running in an environment where a new process cannot be started. */ export function createTerminal(opts: TerminalOptions): Promise /** * Create float window factory for create float window/popup around current * cursor. * Configuration "floatFactory.floatConfig" is used as default float config. * Configuration "coc.preferences.excludeImageLinksInMarkdownDocument" is also used. * * Float windows are automatic reused and hidden on specific events including: * - BufEnter * - InsertEnter * - InsertLeave * - MenuPopupChanged * - CursorMoved * - CursorMovedI * * @param conf Configuration of float window. * @return FloatFactory */ export function createFloatFactory(conf: FloatWinConfig): FloatFactory /** * Reveal message with message type. * * @deprecated Use `window.showErrorMessage`, `window.showWarningMessage` and `window.showInformationMessage` instead. * @param msg Message text to show. * @param messageType Type of message, could be `error` `warning` and `more`, default to `more` */ export function showMessage(msg: string, messageType?: MsgTypes): void /** * Run command in vim terminal for result * * @param cmd Command to run. * @param cwd Cwd of terminal, default to result of |getcwd()|. */ export function runTerminalCommand(cmd: string, cwd?: string, keepfocus?: boolean): Promise /** * Open terminal window. * * @param cmd Command to run. * @param opts Terminal option. * @returns buffer number of terminal. */ export function openTerminal(cmd: string, opts?: OpenTerminalOption): Promise /** * Show quickpick for single item, use `window.menuPick` for menu at current current position. * Use `window.showPickerDialog()` for multiple selection. * * @param items Label list. * @param placeholder Prompt text, default to 'choose by number'. * @returns Index of selected item, or -1 when canceled. * @deprecated use `window.showQuickPick()` instead. */ export function showQuickpick(items: string[], placeholder?: string): Promise /** * Shows a selection list allowing multiple selections. * * @param items An array of strings, or a promise that resolves to an array of strings. * @param options Configures the behavior of the selection list. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ export function showQuickPick(items: readonly string[] | Thenable, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Thenable /** * Shows a selection list. * * @param items An array of strings, or a promise that resolves to an array of strings. * @param options Configures the behavior of the selection list. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selection or `undefined`. */ export function showQuickPick(items: readonly string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable /** * Shows a selection list allowing multiple selections. * * @param items An array of items, or a promise that resolves to an array of items. * @param options Configures the behavior of the selection list. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ export function showQuickPick(items: readonly T[] | Thenable, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Thenable /** * Shows a selection list. * * @param items An array of items, or a promise that resolves to an array of items. * @param options Configures the behavior of the selection list. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected item or `undefined`. */ export function showQuickPick(items: readonly T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable /** * Show menu picker at current cursor position, |inputlist()| is used as fallback. * * @param items Array of texts or menu items. * @param title Optional title of float/popup window. * @param token A token that can be used to signal cancellation. * @returns Selected index (0 based), -1 when canceled. */ export function showMenuPicker(items: string[] | MenuItem[], option?: MenuOption | string, token?: CancellationToken): Promise /** * Prompt user for confirm, a float/popup window would be used when possible, * use vim's |confirm()| function as callback. * * @param title The prompt text. * @returns Result of confirm. */ export function showPrompt(title: string): Promise /** * Show dialog window at the center of screen. * Note that the dialog would always be closed after button click. * * @param config Dialog configuration. * @returns Dialog or null when dialog can't work. */ export function showDialog(config: DialogConfig): Promise /** * Request input from user, `input()` is used when `window.env.dialog` not true. * * @param title Title text of prompt window. * @param defaultValue Default value of input, empty text by default. * @param {InputOptions} option for input window, other preferences are read from user configuration. */ export function requestInput(title: string, defaultValue?: string, option?: InputOptions): Promise /** * Creates and show a {@link InputBox} to let the user enter some text input. * * @return A new {@link InputBox}. */ export function createInputBox(title: string, defaultValue?: string, option?: InputPreference): Promise /** * Creates and show a {@link QuickPick} to let the user pick an item or items from a * list of items of type T. * * Note that in many cases the more convenient {@link window.showQuickPick} * is easier to use. {@link window.createQuickPick} should be used * when {@link window.showQuickPick} does not offer the required flexibility. * * Note that unlike VSCode, promise is returned for wait other inputs finished. * * @param config @deprecated config of quickpick, use properties of QuickPick instance instead. * @return A new {@link QuickPick}. */ export function createQuickPick(config?: QuickPickConfig): Promise> /** * Create statusbar item that would be included in `g:coc_status`. * * @param priority Higher priority item would be shown right. * @param option * @return A new status bar item. */ export function createStatusBarItem(priority?: number, option?: StatusItemOption): StatusBarItem /** * Open local config file */ export function openLocalConfig(): Promise /** * Create a new output channel * * @param name Unique name of output channel. * @returns A new output channel. */ export function createOutputChannel(name: string): OutputChannel /** * Create a {@link TreeView} instance, call `show()` method to render. * * @param viewId Id of the view, used as title of TreeView when title doesn't exist. * @param options Options for creating the {@link TreeView} * @returns a {@link TreeView}. */ export function createTreeView(viewId: string, options: TreeViewOptions): TreeView /** * Reveal buffer of output channel. * * @param name Name of output channel. * @param preserveFocus Preserve window focus when true. */ export function showOutputChannel(name: string, preserveFocus: boolean): void /** * Echo lines at the bottom of vim. * * @param lines Line list. * @param truncate Truncate the lines to avoid 'press enter to continue' when true */ export function echoLines(lines: string[], truncate?: boolean): Promise /** * Get current cursor position (line, character both 0 based). * * @returns Cursor position. */ export function getCursorPosition(): Promise /** * Move cursor to position (line, character both 0 based). * * @param position LSP position. */ export function moveTo(position: Position): Promise /** * Get current cursor character offset in document, * length of line break would always be 1. * * @returns Character offset. */ export function getOffset(): Promise /** * Get screen position of current cursor(relative to editor), * both `row` and `col` are 0 based. * * @returns Cursor screen position. */ export function getCursorScreenPosition(): Promise /** * Show multiple picker at center of screen. * * @param items A set of items that will be rendered as actions in the message. * @param title Title of picker dialog. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ export function showPickerDialog(items: string[], title: string, token?: CancellationToken): Promise /** * Show multiple picker at center of screen. * * @param items A set of items that will be rendered as actions in the message. * @param title Title of picker dialog. * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ export function showPickerDialog(items: T[], title: string, token?: CancellationToken): Promise /** * Show an information message to users. Optionally provide an array of items which will be presented as * clickable buttons. * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ export function showInformationMessage(message: string, ...items: string[]): Promise /** * Show an information message to users. Optionally provide an array of items which will be presented as * clickable buttons. * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ export function showInformationMessage(message: string, ...items: T[]): Promise /** * Show an warning message to users. Optionally provide an array of items which will be presented as * clickable buttons. * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ export function showWarningMessage(message: string, ...items: string[]): Promise /** * Show an warning message to users. Optionally provide an array of items which will be presented as * clickable buttons. * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ export function showWarningMessage(message: string, ...items: T[]): Promise /** * Show an error message to users. Optionally provide an array of items which will be presented as * clickable buttons. * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ export function showErrorMessage(message: string, ...items: string[]): Promise /** * Show an error message to users. Optionally provide an array of items which will be presented as * clickable buttons. * * @param message The message to show. * @param items A set of items that will be rendered as actions in the message. * @return Promise that resolves to the selected item or `undefined` when being dismissed. */ export function showErrorMessage(message: string, ...items: T[]): Promise /** * Show notification window at bottom right of screen. */ export function showNotification(config: NotificationConfig): Promise /** * Show progress in the editor. Progress is shown while running the given callback * and while the promise it returned isn't resolved nor rejected. * * @param task A callback returning a promise. Progress state can be reported with * the provided [progress](#Progress)-object. * * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of * e.g. `10` accounts for `10%` of work done). * * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). * * @return The thenable the task-callback returned. */ export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string increment?: number }>, token: CancellationToken) => Thenable): Promise /** * Get selected range for current document */ export function getSelectedRange(visualmode: string): Promise /** * Visual select range of current document */ export function selectRange(range: Range): Promise /** * Get diff between new highlight items and current highlights requested from vim * * @param {number} bufnr - Buffer number * @param {string} ns - Highlight namespace * @param {HighlightItem[]} items - Highlight items * @param {[number, number] | undefined} region - 0 based start line and end line (end inclusive) * @param {CancellationToken} token - CancellationToken * @returns {Promise} */ export function diffHighlights(bufnr: number, ns: string, items: ExtendedHighlightItem[], region?: [number, number] | undefined, token?: CancellationToken): Promise /** * Apply highlight diffs, normally used with `window.diffHighlights` * * Timer is used to add highlights when there're too many highlight items to add, * the highlight process won't be finished on that case. * * @param {number} bufnr - Buffer name * @param {string} ns - Namespace * @param {number} priority * @param {HighlightDiff} diff * @param {boolean} notify - Use notification, default false. * @returns {Promise} */ export function applyDiffHighlights(bufnr: number, ns: string, priority: number, diff: HighlightDiff, notify?: boolean): Promise /** * Get visible ranges of bufnr, when winid specified, only visible range of winid returned. * Return empty array when buffer is hidden or window with winid not exists. * * @param {number} bufnr - Buffer number * @param {number} winid - Window ID. * @returns {Promise<[number, number][]>} List with [topline, botline], both 1 based and inclusive (returned by getwininfo()). */ export function getVisibleRanges(bufnr: number, winid?: number): Promise<[number, number][]> } // }} // extensions module {{ export interface Logger { readonly category: string log(...args: any[]): void trace(message: any, ...args: any[]): void debug(message: any, ...args: any[]): void info(message: any, ...args: any[]): void warn(message: any, ...args: any[]): void error(message: any, ...args: any[]): void fatal(message: any, ...args: any[]): void mark(message: any, ...args: any[]): void } /** * A memento represents a storage utility. It can store and retrieve * values. */ export interface Memento { /** * Return a value. * * @param key A string. * @return The stored value or `undefined`. */ get(key: string): T | undefined /** * Return a value. * * @param key A string. * @param defaultValue A value that should be returned when there is no * value (`undefined`) with the given key. * @return The stored value or the defaultValue. */ get(key: string, defaultValue: T): T /** * Store a value. The value must be JSON-stringifyable. * * @param key A string. * @param value A value. MUST not contain cyclic references. */ update(key: string, value: any): Promise } export type ExtensionState = 'disabled' | 'loaded' | 'activated' | 'unknown' export enum ExtensionType { Global, Local, SingleFile, Internal } export interface ExtensionJson { name: string main?: string engines: { [key: string]: string } version?: string [key: string]: any } export interface ExtensionInfo { id: string version: string description: string root: string exotic: boolean uri?: string state: ExtensionState isLocal: boolean packageJSON: Readonly } /** * Represents an extension. * * To get an instance of an `Extension` use [getExtension](#extensions.getExtension). */ export interface Extension { /** * The canonical extension identifier in the form of: `publisher.name`. */ readonly id: string /** * The absolute file path of the directory containing this extension. */ readonly extensionPath: string /** * The uri of the directory containing the extension. */ readonly extensionUri: Uri /** * `true` if the extension has been activated. */ readonly isActive: boolean /** * The parsed contents of the extension's package.json. */ readonly packageJSON: any /** * The public API exported by this extension. It is an invalid action * to access this field before this extension has been activated. */ readonly exports: T /** * Activates this extension and returns its public API. * * @return A promise that will resolve when this extension has been activated. */ activate(): Promise } /** * An extension context is a collection of utilities private to an * extension. * * An instance of an `ExtensionContext` is provided as the first * parameter to the `activate`-call of an extension. */ export interface ExtensionContext { /** * An array to which disposables can be added. When this * extension is deactivated the disposables will be disposed. */ subscriptions: Disposable[] /** * The absolute file path of the directory containing the extension. */ extensionPath: string /** * Get the absolute path of a resource contained in the extension. * * @param relativePath A relative path to a resource contained in the extension. * @return The absolute path of the resource. */ asAbsolutePath(relativePath: string): string /** * The absolute directory path for extension to download persist data. * The directory might not exist. */ storagePath: string /** * A memento object that stores state in the context * of the currently opened [workspace](#workspace.workspaceFolders). */ workspaceState: Memento /** * A memento object that stores state independent * of the current opened [workspace](#workspace.workspaceFolders). */ globalState: Memento logger: Logger } export interface PropertyScheme { type: string default: any description: string enum?: string[] items?: any [key: string]: any } export namespace extensions { /** * Fired on extension loaded, extension not activated yet. */ export const onDidLoadExtension: Event> /** * Fired on extension activated. */ export const onDidActiveExtension: Event> /** * Fired with extension id on extension unload. */ export const onDidUnloadExtension: Event /** * Get all loaded extensions, without disabled extensions, extension may not activated. */ export const all: ReadonlyArray> /** * Get an extension by its full identifier in the form of: `publisher.name`. * * @param extensionId An extension identifier. * @return An extension or `undefined`. */ export function getExtensionById(extensionId: string): Extension | undefined /** * Get state of specific extension. */ export function getExtensionState(id: string): ExtensionState /** * Get state of all extensions, including disabled extensions. */ export function getExtensionStates(): Promise /** * Check if extension is activated. */ export function isActivated(id: string): boolean } // }} // listManager module {{ export interface LocationWithLine { uri: string /** * Match text of line. */ line: string /** * Highlight text in line. */ text?: string } export interface AnsiHighlight { /** * Byte indexes, 0 based. */ span: [number, number] hlGroup: string } export interface ListItem { label: string preselect?: boolean filterText?: string /** * A string that should be used when comparing this item * with other items, only used for fuzzy filter. */ sortText?: string location?: Location | LocationWithLine | string data?: any ansiHighlights?: AnsiHighlight[] resolved?: boolean } export type ListMode = 'normal' | 'insert' export type ListMatcher = 'strict' | 'fuzzy' | 'regex' export interface ListOptions { position: string reverse: boolean input: string ignorecase: boolean interactive: boolean sort: boolean mode: ListMode matcher: ListMatcher autoPreview: boolean numberSelect: boolean noQuit: boolean first: boolean } export interface ListContext { /** * Input on list activated. */ input: string /** * Current work directory on activated. */ cwd: string /** * Options of list. */ options: ListOptions /** * Arguments passed to list. */ args: string[] /** * Original window on list invoke. */ window: Window /** * Original buffer on list invoke. */ buffer: Buffer listWindow: Window | null } export interface ListAction { /** * Action name */ name: string /** * Should persist list window on invoke. */ persist?: boolean /** * Should reload list after invoke. */ reload?: boolean /** * Inovke all selected items in parallel. */ parallel?: boolean /** * Support handle multiple items at once. */ multiple?: boolean /** * Tab positioned list should be persisted (no window switch) on action invoke. */ tabPersist?: boolean /** * Item is array of selected items when multiple is true. */ execute: (item: ListItem | ListItem[], context: ListContext) => ProviderResult } export interface MultipleListAction extends Omit { multiple: true execute: (item: ListItem[], context: ListContext) => ProviderResult } export interface ListTask { on(event: 'data', callback: (item: ListItem) => void): void on(event: 'end', callback: () => void): void on(event: 'error', callback: (msg: string | Error) => void): void dispose(): void } export interface ListArgument { key?: string hasValue?: boolean name: string description: string } export interface IList { /** * Unique name of list. */ name: string /** * Default action name. */ defaultAction: string /** * Action list. */ actions: ListAction[] /** * Load list items. */ loadItems(context: ListContext, token: CancellationToken): Promise /** * Should be true when interactive is supported. */ interactive?: boolean /** * Description of list. */ description?: string /** * Detail description, shown in help. */ detail?: string /** * Options supported by list. */ options?: ListArgument[] /** * Resolve list item. */ resolveItem?(item: ListItem): Promise /** * Highlight buffer by vim's syntax commands. */ doHighlight?(): void /** * Called on list unregistered. */ dispose?(): void } export interface PreviewOptions { bufname?: string lines: string[] filetype?: string lnum?: number range?: Range /** * @deprecated not used */ sketch?: boolean } export namespace listManager { /** * Registered list names set. */ export const names: ReadonlyArray /** * Register list, list session can be created by `CocList [name]` after registered. */ export function registerList(list: IList): Disposable } // }} // snippetManager module {{ export interface SnippetSession { isActive: boolean } export interface SnippetEdit { range: Range snippet: string | SnippetString | StringValue } export interface UltiSnipsActions { // pre_expand action code. preExpand?: string // post_expand action code. postExpand?: string // post_jump action code. postJump?: string } export interface UltiSnippetOption { /** * Regex text for regex snippet. */ regex?: string /** * Context code to execute. */ context?: string /** * Do not expand tabs. */ noExpand?: boolean /** * Trim all whitespaces from right side of snippet lines. */ trimTrailingWhitespace?: boolean /** * Remove whitespace immediately before the cursor at the end of a line before jumping to the next tabstop */ removeWhiteSpace?: boolean /** * UltiSnips action codes of the snippet. */ actions: UltiSnipsActions } /** * A snippet string is a template which allows to insert text * and to control the editor cursor when insertion happens. * * A snippet can define tab stops and placeholders with `$1`, `$2` * and `${3:foo}`. `$0` defines the final tab stop, it defaults to * the end of the snippet. Variables are defined with `$name` and * `${name:default value}`. The full snippet syntax is documented * [here](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_creating-your-own-snippets). */ export class SnippetString { /** * The snippet string. */ value: string constructor( /** * The default snippet string. */ value?: string ) /** * Builder-function that appends the given string to * the {@link SnippetString.value `value`} of this snippet string. * * @param string A value to append 'as given'. The string will be escaped. * @return This snippet string. */ appendText(string: string): SnippetString /** * Builder-function that appends a tabstop (`$1`, `$2` etc) to * the {@link SnippetString.value `value`} of this snippet string. * * @param number The number of this tabstop, defaults to an auto-increment * value starting at 1. * @return This snippet string. */ appendTabstop(number?: number): SnippetString /** * Builder-function that appends a placeholder (`${1:value}`) to * the {@link SnippetString.value `value`} of this snippet string. * * @param value The value of this placeholder - either a string or a function * with which a nested snippet can be created. * @param number The number of this tabstop, defaults to an auto-increment * value starting at 1. * @return This snippet string. */ appendPlaceholder(value: string | ((snippet: SnippetString) => any), number?: number): SnippetString /** * Builder-function that appends a choice (`${1|a,b,c|}`) to * the {@link SnippetString.value `value`} of this snippet string. * * @param values The values for choices - the array of strings * @param number The number of this tabstop, defaults to an auto-increment * value starting at 1. * @return This snippet string. */ appendChoice(values: string[], number?: number): SnippetString /** * Builder-function that appends a variable (`${VAR}`) to * the {@link SnippetString.value `value`} of this snippet string. * * @param name The name of the variable - excluding the `$`. * @param defaultValue The default value which is used when the variable name cannot * be resolved - either a string or a function with which a nested snippet can be created. * @return This snippet string. */ appendVariable(name: string, defaultValue: string | ((snippet: SnippetString) => any)): SnippetString } /** * Manage snippet sessions. */ export namespace snippetManager { /** * Get snippet session by bufnr, only returns active session. */ export function getSession(bufnr: number): SnippetSession | undefined /** * Resolve snippet string to text. */ export function resolveSnippet(body: string, ultisnip?: UltiSnippetOption): Promise /** * Insert snippet to specific buffer, ultisnips not supported, and the placeholder is not selected. * * @param bufnr Buffer number for snippet to insert. * @param snippet Textmate snippet or snippet string. * @param range Range to replace. * @param insertTextMode The insert text mode. * @returns Whether the snippet is activated. */ export function insertBufferSnippet(bufnr: number, snippet: string | SnippetString, range: Range, insertTextMode?: InsertTextMode): Promise /** * Insert snippet to current buffer. * * @param snippet Textmate snippet string. * @param select Not select first placeholder when false, default `true`. * @param range Replace range, insert to current cursor position when undefined. * @param insertTextMode The insert text mode. * @param ultisnip Option of UltiSnips snippet. * @returns Whether the snippet is activated. */ export function insertSnippet(snippet: string | SnippetString, select?: boolean, range?: Range, insertTextMode?: InsertTextMode, ultisnip?: UltiSnippetOption): Promise /** * Insert multiple snippets to a specific buffer, the buffer must be * attached buffer. The buffer could be hidden, ranges of inserted snippets * should not have overlap, snippets are inserted as nested snippets of a * top snippet. No ultisnip snippet support, selection is disabled by * default. When not selected, the first placeholder is selected on * BufEnter event. * * @param {number} bufnr - Buffer number of attached buffer. * @param {SnippetEdit[]} edits - snippet edits with range and snippet. * @param {boolean} [select] - select the first placeholder when bufnr is * current buffer. * @returns {Promise} True when snippet is activated. */ export function insertBufferSnippets(bufnr: number, edits: SnippetEdit[], select?: boolean): Promise /** * Jump to next placeholder, only works when snippet session activated. */ export function nextPlaceholder(): Promise /** * Jump to previous placeholder, only works when snippet session activated. */ export function previousPlaceholder(): Promise /** * Cancel snippet session of current buffer, does nothing when no session activated. */ export function cancel(): void /** * Check if snippet activated for bufnr. */ export function isActivated(bufnr: number): boolean } // }} // diagnosticManager module {{ export interface DiagnosticItem { file: string lnum: number col: number source: string code: string | number message: string severity: string level: number location: Location } export enum DiagnosticKind { Syntax, Semantic, Suggestion, } /** * A diagnostics collection is a container that manages a set of * [diagnostics](#Diagnostic). Diagnostics are always scopes to a * diagnostics collection and a resource. * * To get an instance of a `DiagnosticCollection` use * [createDiagnosticCollection](#languages.createDiagnosticCollection). */ export interface DiagnosticCollection { /** * The name of this diagnostic collection, for instance `typescript`. Every diagnostic * from this collection will be associated with this name. Also, the task framework uses this * name when defining [problem matchers](https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher). */ readonly name: string /** * Assign diagnostics for given resource. Will replace * existing diagnostics for that resource. * * @param uri A resource identifier. * @param diagnostics Array of diagnostics or `undefined` */ set(uri: string, diagnostics: Diagnostic[] | null): void /** * Replace all entries in this collection. * * Diagnostics of multiple tuples of the same uri will be merged, e.g * `[[file1, [d1]], [file1, [d2]]]` is equivalent to `[[file1, [d1, d2]]]`. * If a diagnostics item is `undefined` as in `[file1, undefined]` * all previous but not subsequent diagnostics are removed. * * @param entries An array of tuples, like `[[file1, [d1, d2]], [file2, [d3, d4, d5]]]`, or `undefined`. */ set(entries: [string, Diagnostic[] | null][] | string, diagnostics?: Diagnostic[]): void /** * Remove all diagnostics from this collection that belong * to the provided `uri`. The same as `#set(uri, undefined)`. * * @param uri A resource identifier. */ delete(uri: string): void /** * Remove all diagnostics from this collection. The same * as calling `#set(undefined)` */ clear(): void /** * Iterate over each entry in this collection. * * @param callback Function to execute for each entry. * @param thisArg The `this` context used when invoking the handler function. */ forEach(callback: (uri: string, diagnostics: Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void /** * Get the diagnostics for a given resource. *Note* that you cannot * modify the diagnostics-array returned from this call. * * @param uri A resource identifier. * @returns An immutable array of [diagnostics](#Diagnostic) or `undefined`. */ get(uri: string): Diagnostic[] | undefined /** * Check if this collection contains diagnostics for a * given resource. * * @param uri A resource identifier. * @returns `true` if this collection has diagnostic for the given resource. */ has(uri: string): boolean /** * Dispose and free associated resources. Calls * [clear](#DiagnosticCollection.clear). */ dispose(): void } export interface DiagnosticEventParams { bufnr: number uri: string diagnostics: ReadonlyArray } export namespace diagnosticManager { export const onDidRefresh: Event /** * Create collection by name */ export function create(name: string): DiagnosticCollection /** * Get readonly diagnostics for uri */ export function getDiagnostics(uri: string): { [collection: string]: Diagnostic[] } /** * Get readonly diagnostics by document and range. */ export function getDiagnosticsInRange(doc: TextDocumentIdentifier, range: Range): ReadonlyArray /** * Get all sorted diagnostics */ export function getDiagnosticList(): Promise> /** * All diagnostics at current cursor position. */ export function getCurrentDiagnostics(): Promise> /** * Get diagnostic collection. */ export function getCollectionByName(name: string): DiagnosticCollection } // }} // language client {{ export type ProgressToken = number | string export interface WorkDoneProgressBegin { kind: 'begin' /** * Mandatory title of the progress operation. Used to briefly inform about * the kind of operation being performed. * * Examples: "Indexing" or "Linking dependencies". */ title: string /** * Controls if a cancel button should show to allow the user to cancel the * long running operation. Clients that don't support cancellation are allowed * to ignore the setting. */ cancellable?: boolean /** * Optional, more detailed associated progress message. Contains * complementary information to the `title`. * * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". * If unset, the previous progress message (if any) is still valid. */ message?: string /** * Optional progress percentage to display (value 100 is considered 100%). * If not provided infinite progress is assumed and clients are allowed * to ignore the `percentage` value in subsequent in report notifications. * * The value should be steadily rising. Clients are free to ignore values * that are not following this rule. */ percentage?: number } export interface WorkDoneProgressReport { kind: 'report' /** * Controls enablement state of a cancel button. This property is only valid if a cancel * button got requested in the `WorkDoneProgressStart` payload. * * Clients that don't support cancellation or don't support control the button's * enablement state are allowed to ignore the setting. */ cancellable?: boolean /** * Optional, more detailed associated progress message. Contains * complementary information to the `title`. * * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". * If unset, the previous progress message (if any) is still valid. */ message?: string /** * Optional progress percentage to display (value 100 is considered 100%). * If not provided infinite progress is assumed and clients are allowed * to ignore the `percentage` value in subsequent in report notifications. * * The value should be steadily rising. Clients are free to ignore values * that are not following this rule. */ percentage?: number } export interface WorkDoneProgressEnd { kind: 'end' /** * Optional, a final message indicating to for example indicate the outcome * of the operation. */ message?: string } /** * The file event type */ export namespace FileChangeType { /** * The file got created. */ const Created = 1 /** * The file got changed. */ const Changed = 2 /** * The file got deleted. */ const Deleted = 3 } export type FileChangeType = 1 | 2 | 3 /** * An event describing a file change. */ export interface FileEvent { /** * The file's uri. */ uri: string /** * The change type. */ type: FileChangeType } /** * An action to be performed when the connection is producing errors. */ export enum ErrorAction { /** * Continue running the server. */ Continue = 1, /** * Shutdown the server. */ Shutdown = 2 } /** * An action to be performed when the connection to a server got closed. */ export enum CloseAction { /** * Don't restart the server. The connection stays closed. */ DoNotRestart = 1, /** * Restart the server. */ Restart = 2 } export interface CloseHandlerResult { /** * The action to take. */ action: CloseAction /** * An optional message to be presented to the user. */ message?: string /** * If set to true the client assumes that the corresponding * close handler has presented an appropriate message to the * user and the message will only be log to the client's * output channel. */ handled?: boolean } export interface ErrorHandlerResult { /** * The action to take. */ action: ErrorAction /** * An optional message to be presented to the user. */ message?: string /** * If set to true the client assumes that the corresponding * error handler has presented an appropriate message to the * user and the message will only be log to the client's * output channel. */ handled?: boolean } /** * A pluggable error handler that is invoked when the connection is either * producing errors or got closed. */ export interface ErrorHandler { /** * An error has occurred while writing or reading from the connection. * * @param error - the error received * @param message - the message to be delivered to the server if know. * @param count - a count indicating how often an error is received. Will * be reset if a message got successfully send or received. */ error(error: Error, message: { jsonrpc: string }, count: number): ErrorAction | ErrorHandlerResult | Promise /** * The connection to the server got closed. * Use CloseHandlerResult should be preferred. */ closed(): CloseAction | CloseHandlerResult | Promise } export interface InitializationFailedHandler { (error: Error | any): boolean } export interface SynchronizeOptions { /** * @deprecated Use the new pull model (`workspace/configuration` request) */ configurationSection?: string | string[] fileEvents?: FileSystemWatcher | FileSystemWatcher[] } export enum RevealOutputChannelOn { Debug = 0, Info = 1, Warn = 2, Error = 3, Never = 4 } export interface ConfigurationItem { /** * The scope to get the configuration section for. */ scopeUri?: string /** * The configuration section asked for. */ section?: string } export type HandlerResult = R | ResponseError | Thenable | Thenable> | Thenable> export interface RequestHandler { (params: P, token: CancellationToken): HandlerResult } export interface RequestHandler0 { (token: CancellationToken): HandlerResult } /** * The parameters of a configuration request. */ export interface ConfigurationParams { items: ConfigurationItem[] } export interface ConfigurationWorkspaceMiddleware { configuration?: (params: ConfigurationParams, token: CancellationToken, next: RequestHandler) => HandlerResult } export interface WorkspaceFolderWorkspaceMiddleware { workspaceFolders?: (token: CancellationToken, next: RequestHandler0) => HandlerResult didChangeWorkspaceFolders?: NextSignature> } export interface ProvideTypeDefinitionSignature { ( this: void, document: LinesTextDocument, position: Position, token: CancellationToken ): ProviderResult } export interface TypeDefinitionMiddleware { provideTypeDefinition?: ( this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideTypeDefinitionSignature ) => ProviderResult } export interface ProvideImplementationSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ImplementationMiddleware { provideImplementation?: (this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideImplementationSignature) => ProviderResult } export type ProvideDocumentColorsSignature = (document: LinesTextDocument, token: CancellationToken) => ProviderResult export type ProvideColorPresentationSignature = ( color: Color, context: { document: LinesTextDocument; range: Range }, token: CancellationToken ) => ProviderResult export interface ColorProviderMiddleware { provideDocumentColors?: ( this: void, document: LinesTextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature ) => ProviderResult provideColorPresentations?: ( this: void, color: Color, context: { document: LinesTextDocument; range: Range }, token: CancellationToken, next: ProvideColorPresentationSignature ) => ProviderResult } export interface ProvideDeclarationSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface DeclarationMiddleware { provideDeclaration?: (this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideDeclarationSignature) => ProviderResult } export type ProvideFoldingRangeSignature = ( this: void, document: LinesTextDocument, context: FoldingContext, token: CancellationToken ) => ProviderResult export interface FoldingRangeProviderMiddleware { provideFoldingRanges?: ( this: void, document: LinesTextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature ) => ProviderResult } export interface PrepareCallHierarchySignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface CallHierarchyIncomingCallsSignature { (this: void, item: CallHierarchyItem, token: CancellationToken): ProviderResult } export interface CallHierarchyOutgoingCallsSignature { (this: void, item: CallHierarchyItem, token: CancellationToken): ProviderResult } export interface CallHierarchyMiddleware { prepareCallHierarchy?: ( this: void, document: LinesTextDocument, positions: Position, token: CancellationToken, next: PrepareCallHierarchySignature ) => ProviderResult provideCallHierarchyIncomingCalls?: ( this: void, item: CallHierarchyItem, token: CancellationToken, next: CallHierarchyIncomingCallsSignature ) => ProviderResult provideCallHierarchyOutgoingCalls?: ( this: void, item: CallHierarchyItem, token: CancellationToken, next: CallHierarchyOutgoingCallsSignature ) => ProviderResult } export interface DocumentSemanticsTokensSignature { (this: void, document: LinesTextDocument, token: CancellationToken): ProviderResult } export interface DocumentSemanticsTokensEditsSignature { (this: void, document: LinesTextDocument, previousResultId: string, token: CancellationToken): ProviderResult } export interface DocumentRangeSemanticTokensSignature { (this: void, document: LinesTextDocument, range: Range, token: CancellationToken): ProviderResult } export interface SemanticTokensMiddleware { provideDocumentSemanticTokens?: ( this: void, document: LinesTextDocument, token: CancellationToken, next: DocumentSemanticsTokensSignature ) => ProviderResult provideDocumentSemanticTokensEdits?: ( this: void, document: LinesTextDocument, previousResultId: string, token: CancellationToken, next: DocumentSemanticsTokensEditsSignature ) => ProviderResult provideDocumentRangeSemanticTokens?: ( this: void, document: LinesTextDocument, range: Range, token: CancellationToken, next: DocumentRangeSemanticTokensSignature ) => ProviderResult } export interface FileOperationsMiddleware { didCreateFiles?: NextSignature> willCreateFiles?: NextSignature> didRenameFiles?: NextSignature> willRenameFiles?: NextSignature> didDeleteFiles?: NextSignature> willDeleteFiles?: NextSignature> } export interface ProvideLinkedEditingRangeSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface LinkedEditingRangeMiddleware { provideLinkedEditingRange?: ( this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideLinkedEditingRangeSignature ) => ProviderResult } export interface ProvideSelectionRangeSignature { (this: void, document: LinesTextDocument, positions: Position[], token: CancellationToken): ProviderResult } export interface SelectionRangeProviderMiddleware { provideSelectionRanges?: (this: void, document: LinesTextDocument, positions: Position[], token: CancellationToken, next: ProvideSelectionRangeSignature) => ProviderResult } export type ProvideDiagnosticSignature = (this: void, document: TextDocument, previousResultId: string | undefined, token: CancellationToken) => ProviderResult export type ProvideWorkspaceDiagnosticSignature = (this: void, resultIds: PreviousResultId[], token: CancellationToken, resultReporter: ResultReporter) => ProviderResult export interface DiagnosticProviderMiddleware { provideDiagnostics?: (this: void, document: TextDocument, previousResultId: string | undefined, token: CancellationToken, next: ProvideDiagnosticSignature) => ProviderResult provideWorkspaceDiagnostics?: (this: void, resultIds: PreviousResultId[], token: CancellationToken, resultReporter: ResultReporter, next: ProvideWorkspaceDiagnosticSignature) => ProviderResult } export interface HandleWorkDoneProgressSignature { (this: void, token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd): void } export interface HandleDiagnosticsSignature { (this: void, uri: string, diagnostics: Diagnostic[]): void } export interface ProvideCompletionItemsSignature { (this: void, document: LinesTextDocument, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult } export interface ResolveCompletionItemSignature { (this: void, item: CompletionItem, token: CancellationToken): ProviderResult } export interface ProvideHoverSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ProvideSignatureHelpSignature { (this: void, document: LinesTextDocument, position: Position, context: SignatureHelpContext, token: CancellationToken): ProviderResult } export interface ProvideDefinitionSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ProvideReferencesSignature { (this: void, document: LinesTextDocument, position: Position, options: { includeDeclaration: boolean }, token: CancellationToken): ProviderResult } export interface ProvideDocumentHighlightsSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ProvideDocumentSymbolsSignature { (this: void, document: LinesTextDocument, token: CancellationToken): ProviderResult } export interface ProvideWorkspaceSymbolsSignature { (this: void, query: string, token: CancellationToken): ProviderResult } export interface ProvideCodeActionsSignature { (this: void, document: LinesTextDocument, range: Range, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | CodeAction)[]> } export interface ResolveCodeActionSignature { (this: void, item: CodeAction, token: CancellationToken): ProviderResult } export interface ProvideCodeLensesSignature { (this: void, document: LinesTextDocument, token: CancellationToken): ProviderResult } export interface ResolveCodeLensSignature { (this: void, codeLens: CodeLens, token: CancellationToken): ProviderResult } export interface ProvideDocumentFormattingEditsSignature { (this: void, document: LinesTextDocument, options: FormattingOptions, token: CancellationToken): ProviderResult } export interface ProvideDocumentRangeFormattingEditsSignature { (this: void, document: LinesTextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult } export interface ProvideOnTypeFormattingEditsSignature { (this: void, document: LinesTextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): ProviderResult } export interface PrepareRenameSignature { (this: void, document: LinesTextDocument, position: Position, token: CancellationToken): ProviderResult } export interface ProvideRenameEditsSignature { (this: void, document: LinesTextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult } export interface ProvideDocumentLinksSignature { (this: void, document: LinesTextDocument, token: CancellationToken): ProviderResult } export interface ResolveDocumentLinkSignature { (this: void, link: DocumentLink, token: CancellationToken): ProviderResult } export interface ExecuteCommandSignature { (this: void, command: string, args: any[]): ProviderResult } export interface NextSignature { (this: void, data: P, next: (data: P) => R): R } export interface DidChangeConfigurationSignature { (this: void, sections: string[] | undefined): void } export interface DidChangeWatchedFileSignature { (this: void, event: FileEvent): void } export interface ProvideInlineCompletionItemsSignature { (this: void, document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult } export interface _WorkspaceMiddleware { didChangeConfiguration?: (this: void, sections: string[] | undefined, next: DidChangeConfigurationSignature) => Promise didChangeWatchedFile?: (this: void, event: FileEvent, next: DidChangeWatchedFileSignature) => void handleApplyEdit?: (this: void, params: ApplyWorkspaceEditParams, next: RequestHandler) => HandlerResult } export type WorkspaceMiddleware = _WorkspaceMiddleware & ConfigurationWorkspaceMiddleware & WorkspaceFolderWorkspaceMiddleware & FileOperationsMiddleware /** * Params to show a document. * * @since 3.16.0 */ export interface ShowDocumentParams { /** * The document uri to show. */ uri: string /** * Indicates to show the resource in an external program. * To show for example `https://code.visualstudio.com/` * in the default WEB browser set `external` to `true`. */ external?: boolean /** * An optional property to indicate whether the editor * showing the document should take focus or not. * Clients might ignore this property if an external * program in started. */ takeFocus?: boolean /** * An optional selection range if the document is a text * document. Clients might ignore the property if an * external program is started or the file is not a text * file. */ selection?: Range } /** * The result of an show document request. * * @since 3.16.0 */ export interface ShowDocumentResult { /** * A boolean indicating if the show was successful. */ success: boolean } /** * General parameters to register for a notification or to register a provider. */ export interface Registration { /** * The id used to register the request. The id can be used to deregister * the request again. */ id: string /** * The method / capability to register for. */ method: string /** * Options necessary for the registration. */ registerOptions?: LSPAny } export interface RegistrationParams { registrations: Registration[] } /** * General parameters to unregister a request or notification. */ export interface Unregistration { /** * The id used to unregister the request or notification. Usually an id * provided during the register request. */ id: string /** * The method to unregister for. */ method: string } export interface UnregistrationParams { unregisterations: Unregistration[] } export interface _WindowMiddleware { showDocument?: ( params: ShowDocumentParams, token: CancellationToken, next: RequestHandler ) => Promise } export type WindowMiddleware = _WindowMiddleware /** * The Middleware lets extensions intercept the request and notifications send and received * from the server */ interface _Middleware { didOpen?: NextSignature> didChange?: NextSignature> willSave?: NextSignature> willSaveWaitUntil?: NextSignature> didSave?: NextSignature> didClose?: NextSignature> handleDiagnostics?: (this: void, uri: string, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => void provideCompletionItem?: (this: void, document: LinesTextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => ProviderResult resolveCompletionItem?: (this: void, item: CompletionItem, token: CancellationToken, next: ResolveCompletionItemSignature) => ProviderResult provideHover?: (this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) => ProviderResult provideSignatureHelp?: (this: void, document: LinesTextDocument, position: Position, context: SignatureHelpContext, token: CancellationToken, next: ProvideSignatureHelpSignature) => ProviderResult provideDefinition?: (this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideDefinitionSignature) => ProviderResult provideReferences?: (this: void, document: LinesTextDocument, position: Position, options: { includeDeclaration: boolean }, token: CancellationToken, next: ProvideReferencesSignature) => ProviderResult provideDocumentHighlights?: (this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: ProvideDocumentHighlightsSignature) => ProviderResult provideDocumentSymbols?: (this: void, document: LinesTextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature) => ProviderResult provideWorkspaceSymbols?: (this: void, query: string, token: CancellationToken, next: ProvideWorkspaceSymbolsSignature) => ProviderResult provideCodeActions?: (this: void, document: LinesTextDocument, range: Range, context: CodeActionContext, token: CancellationToken, next: ProvideCodeActionsSignature) => ProviderResult<(Command | CodeAction)[]> handleWorkDoneProgress?: (this: void, token: ProgressToken, params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd, next: HandleWorkDoneProgressSignature) => void handleRegisterCapability?: (this: void, params: RegistrationParams, next: RequestHandler) => Promise handleUnregisterCapability?: (this: void, params: UnregistrationParams, next: RequestHandler) => Promise resolveCodeAction?: (this: void, item: CodeAction, token: CancellationToken, next: ResolveCodeActionSignature) => ProviderResult provideCodeLenses?: (this: void, document: LinesTextDocument, token: CancellationToken, next: ProvideCodeLensesSignature) => ProviderResult resolveCodeLens?: (this: void, codeLens: CodeLens, token: CancellationToken, next: ResolveCodeLensSignature) => ProviderResult provideDocumentFormattingEdits?: (this: void, document: LinesTextDocument, options: FormattingOptions, token: CancellationToken, next: ProvideDocumentFormattingEditsSignature) => ProviderResult provideDocumentRangeFormattingEdits?: (this: void, document: LinesTextDocument, range: Range, options: FormattingOptions, token: CancellationToken, next: ProvideDocumentRangeFormattingEditsSignature) => ProviderResult provideOnTypeFormattingEdits?: (this: void, document: LinesTextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken, next: ProvideOnTypeFormattingEditsSignature) => ProviderResult prepareRename?: (this: void, document: LinesTextDocument, position: Position, token: CancellationToken, next: PrepareRenameSignature) => ProviderResult provideRenameEdits?: (this: void, document: LinesTextDocument, position: Position, newName: string, token: CancellationToken, next: ProvideRenameEditsSignature) => ProviderResult provideDocumentLinks?: (this: void, document: LinesTextDocument, token: CancellationToken, next: ProvideDocumentLinksSignature) => ProviderResult resolveDocumentLink?: (this: void, link: DocumentLink, token: CancellationToken, next: ResolveDocumentLinkSignature) => ProviderResult executeCommand?: (this: void, command: string, args: any[], next: ExecuteCommandSignature) => ProviderResult workspace?: WorkspaceMiddleware window?: WindowMiddleware } // A general middleware is applied to both requests and notifications interface GeneralMiddleware { sendRequest?( this: void, type: string | MessageSignature, param: P | undefined, token: CancellationToken | undefined, next: (type: string | MessageSignature, param?: P, token?: CancellationToken) => Promise, ): Promise sendNotification?( this: void, type: string | MessageSignature, next: (type: string | MessageSignature, params?: R) => Promise, params: R ): Promise } export interface ProvideTextDocumentContentSignature { (this: void, uri: Uri, token: CancellationToken): ProviderResult } export interface TextDocumentContentMiddleware { provideTextDocumentContent?: (this: void, uri: Uri, token: CancellationToken, next: ProvideTextDocumentContentSignature) => ProviderResult } export interface InlineCompletionMiddleware { provideInlineCompletionItems?: (this: void, document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken, next: ProvideInlineCompletionItemsSignature) => ProviderResult } export type Middleware = _Middleware & TypeDefinitionMiddleware & ImplementationMiddleware & ColorProviderMiddleware & DeclarationMiddleware & FoldingRangeProviderMiddleware & CallHierarchyMiddleware & SemanticTokensMiddleware & LinkedEditingRangeMiddleware & SelectionRangeProviderMiddleware & DiagnosticProviderMiddleware & GeneralMiddleware & TextDocumentContentMiddleware & InlineCompletionMiddleware export interface ConnectionOptions { maxRestartCount?: number } export enum DiagnosticPullMode { onType = 'onType', onSave = 'onSave', onFocus = 'onFocus' } export interface DiagnosticPullOptions { /** * Whether to pull for diagnostics on document change. * Default to "pullDiagnostic.onChange" configuration. */ onChange?: boolean /** * Whether to pull for diagnostics on editor focus. */ onFocus?: boolean /** * Whether to pull for diagnostics on document save. * Default to "pullDiagnostic.onSave" configuration. */ onSave?: boolean /** * Whether to pull for workspace diagnostics when possible. * Default to "pullDiagnostic.workspace" configuration. */ workspace?: boolean /** * Minimatch patterns to match full filepath that should be ignored for pullDiagnostic. * Default to "pullDiagnostic.ignored" configuration. */ ignored?: string[] /** * An optional filter method that is consulted when triggering a * diagnostic pull during document change or document save. * * The document gets filtered if the method returns `true`. * * @param document the document that changes or got save * @param mode the mode */ filter?(document: { uri: string, languageId: string }, mode: 'onType' | 'onSave'): boolean /** * An optional match method that is consulted when pulling for diagnostics * when only a URI is known (e.g. for not instantiated tabs) * * The method should return `true` if the document selector matches the * given resource. See also the `vscode.languages.match` function. * * @param documentSelector The document selector. * @param resource The resource. * @returns whether the resource is matched by the given document selector. */ match?(documentSelector: DocumentSelector, resource: Uri): boolean } export interface URIConverter { (value: Uri): string } export interface LanguageClientOptions { ignoredRootPaths?: string[] disableSnippetCompletion?: boolean disableDynamicRegister?: boolean disabledFeatures?: string[] formatterPriority?: number documentSelector?: DocumentSelector | string[] synchronize?: SynchronizeOptions diagnosticCollectionName?: string outputChannelName?: string outputChannel?: OutputChannel traceOutputChannel?: OutputChannel revealOutputChannelOn?: RevealOutputChannelOn /** * The encoding use to read stdout and stderr. Defaults * to 'utf8' if omitted. */ stdioEncoding?: string // converter used to decode uri. uriConverter?: { code2Protocol: URIConverter } initializationOptions?: any | (() => any) initializationFailedHandler?: InitializationFailedHandler progressOnInitialization?: boolean errorHandler?: ErrorHandler middleware?: Middleware workspaceFolder?: WorkspaceFolder connectionOptions?: ConnectionOptions diagnosticPullOptions?: DiagnosticPullOptions textSynchronization?: { /** * Delays sending the open notification until one of the following * conditions becomes `true`: * - document is visible in the editor. * - any of the other notifications or requests is sent to the server, except * a closed notification for the pending document. */ delayOpenNotifications?: boolean } markdown?: { isTrusted?: boolean supportHtml?: boolean } } export enum State { Stopped = 1, Running = 2, Starting = 3, StartFailed = 4, } export interface StateChangeEvent { oldState: State newState: State } export interface RegistrationData { id: string registerOptions: T } export type FeatureState = { kind: 'document' /** * The features's id. This is usually the method names used during * registration. */ id: string /** * Has active registrations. */ registrations: boolean /** * A registration matches an open document. */ matches: boolean } | { kind: 'workspace' /** * The features's id. This is usually the method names used during * registration. */ id: string /** * Has active registrations. */ registrations: boolean } | { kind: 'window' /** * The features's id. This is usually the method names used during * registration. */ id: string /** * Has active registrations. */ registrations: boolean } | { kind: 'static' } /** * A static feature. A static feature can't be dynamically activate via the * server. It is wired during the initialize sequence. */ export interface StaticFeature { /** * Called to fill the initialize params. * * @params the initialize params. */ fillInitializeParams?: (params: object) => void /** * Called to fill in the client capabilities this feature implements. * * @param capabilities The client capabilities to fill. */ fillClientCapabilities(capabilities: object): void /** * A preflight where the server capabilities are shown to all features * before a feature is actually initialized. This allows feature to * capture some state if they are a pre-requisite for other features. * * @param capabilities the server capabilities * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ preInitialize?: (capabilities: object, documentSelector: DocumentSelector | undefined) => void /** * Initialize the feature. This method is called on a feature instance * when the client has successfully received the initialize request from * the server and before the client sends the initialized notification * to the server. * * @param capabilities the server capabilities * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ initialize(capabilities: object, documentSelector: DocumentSelector | undefined): void /** * Returns the state the feature is in. */ getState?(): FeatureState /** * Called when the client is stopped to dispose this feature. Usually a feature * unregisters listeners registered hooked up with the VS Code extension host. */ dispose(): void } /** * A dynamic feature can be activated via the server. */ export interface DynamicFeature { /** * Called to fill the initialize params. * * @params the initialize params. */ fillInitializeParams?: (params: InitializeParams) => void /** * Called to fill in the client capabilities this feature implements. * * @param capabilities The client capabilities to fill. */ fillClientCapabilities(capabilities: any): void /** * Initialize the feature. This method is called on a feature instance * when the client has successfully received the initialize request from * the server and before the client sends the initialized notification * to the server. * * @param capabilities the server capabilities. * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ initialize(capabilities: object, documentSelector: DocumentSelector | undefined): void /** * A preflight where the server capabilities are shown to all features * before a feature is actually initialized. This allows feature to * capture some state if they are a pre-requisite for other features. * * @param capabilities the server capabilities * @param documentSelector the document selector pass to the client's constructor. * May be `undefined` if the client was created without a selector. */ preInitialize?: (capabilities: object, documentSelector: DocumentSelector | undefined) => void /** * The signature (e.g. method) for which this features support dynamic activation / registration. */ registrationType: RegistrationType /** * Is called when the server send a register request for the given message. * * @param data additional registration data as defined in the protocol. */ register(data: RegistrationData): void /** * Is called when the server wants to unregister a feature. * * @param id the id used when registering the feature. */ unregister(id: string): void /** * Returns the state the feature is in. */ getState?(): FeatureState /** * Called when the client is stopped to dispose this feature. Usually a feature * unregisters listeners registered hooked up with the VS Code extension host. */ dispose(): void } class ParameterStructures { private readonly kind /** * The parameter structure is automatically inferred on the number of parameters * and the parameter type in case of a single param. */ static readonly auto: ParameterStructures /** * Forces `byPosition` parameter structure. This is useful if you have a single * parameter which has a literal type. */ static readonly byPosition: ParameterStructures /** * Forces `byName` parameter structure. This is only useful when having a single * parameter. The library will report errors if used with a different number of * parameters. */ static readonly byName: ParameterStructures private constructor() static is(value: any): value is ParameterStructures toString(): string } /** * An interface to type messages. */ export interface MessageSignature { readonly method: string readonly numberOfParams: number readonly parameterStructures: ParameterStructures } /** * * An abstract implementation of a MessageType. */ abstract class AbstractMessageSignature implements MessageSignature { readonly method: string readonly numberOfParams: number constructor(method: string, numberOfParams: number) get parameterStructures(): ParameterStructures } /** * Classes to type request response pairs */ export class RequestType0 extends AbstractMessageSignature { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly _: [R, E, _EM] | undefined constructor(method: string) } export class RequestType extends AbstractMessageSignature { private _parameterStructures /** * Clients must not use this property. It is here to ensure correct typing. */ readonly _: [P, R, E, _EM] | undefined constructor(method: string, _parameterStructures?: ParameterStructures) get parameterStructures(): ParameterStructures } export class NotificationType

extends AbstractMessageSignature { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly _: [P, _EM] | undefined constructor(method: string) } export class NotificationType0 extends AbstractMessageSignature { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly _: [_EM] | undefined constructor(method: string) } export interface InitializeParams { /** * The process Id of the parent process that started * the server. */ processId: number | null /** * Information about the client * * @since 3.15.0 */ clientInfo?: { /** * The name of the client as defined by the client. */ name: string /** * The client's version as defined by the client. */ version?: string } /** * The rootPath of the workspace. Is null * if no folder is open. * * @deprecated in favour of rootUri. */ rootPath?: string | null /** * The rootUri of the workspace. Is null if no * folder is open. If both `rootPath` and `rootUri` are set * `rootUri` wins. * * @deprecated in favour of workspaceFolders. */ rootUri: string | null /** * The capabilities provided by the client (editor or tool) */ capabilities: any /** * User provided initialization options. */ initializationOptions?: any /** * The initial trace setting. If omitted trace is disabled ('off'). */ trace?: 'off' | 'messages' | 'verbose' /** * An optional token that a server can use to report work done progress. */ workDoneToken?: ProgressToken } class RegistrationType { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly ____: [RO, _EM] | undefined readonly method: string constructor(method: string) } /** * The result returned from an initialize request. */ export interface InitializeResult { /** * The capabilities the language server provides. */ capabilities: any /** * Information about the server. * * @since 3.15.0 */ serverInfo?: { /** * The name of the server as defined by the server. */ name: string /** * The servers's version as defined by the server. */ version?: string } /** * Custom initialization results. */ [custom: string]: any } export interface NotificationFeature { /** * Triggers the corresponding RPC method. */ getProvider(document: { uri: string, languageId: string }): { send: T } } export interface ExecutableOptions { cwd?: string env?: any detached?: boolean shell?: boolean } export interface Executable { command: string args?: string[] options?: ExecutableOptions } export interface ForkOptions { cwd?: string env?: any execPath?: string encoding?: string execArgv?: string[] } export interface StreamInfo { writer: NodeJS.WritableStream reader: NodeJS.ReadableStream detached?: boolean } export enum TransportKind { stdio = 0, ipc = 1, pipe = 2, socket = 3 } export interface SocketTransport { kind: TransportKind.socket port: number } export interface NodeModule { module: string transport?: TransportKind | SocketTransport args?: string[] runtime?: string options?: ForkOptions } export interface ChildProcessInfo { process: cp.ChildProcess detached: boolean } export interface PartialMessageInfo { readonly messageToken: number readonly waitingTime: number } export interface MessageReader { readonly onError: Event readonly onClose: Event readonly onPartialMessage: Event listen(callback: (data: { jsonrpc: string }) => void): void dispose(): void } export interface MessageWriter { readonly onError: Event<[Error, { jsonrpc: string } | undefined, number | undefined]> readonly onClose: Event write(msg: { jsonrpc: string }): void dispose(): void } export class NullLogger { constructor() error(message: string): void warn(message: string): void info(message: string): void log(message: string): void } export interface MessageTransports { reader: MessageReader writer: MessageWriter detached?: boolean } export namespace MessageTransports { /** * Checks whether the given value conforms to the [MessageTransports](#MessageTransports) interface. */ function is(value: any): value is MessageTransports } export type ServerOptions = Executable | NodeModule | { run: Executable debug: Executable } | { run: NodeModule debug: NodeModule } | (() => Promise) export interface _EM { _$endMarker$_: number } export class ProgressType { /** * Clients must not use these properties. They are here to ensure correct typing. * in TypeScript */ readonly __?: [PR, _EM] readonly _pr?: PR constructor() } export enum Trace { Off = 0, Messages = 1, Verbose = 2 } export interface RequestProtocolSignature { method: string numberOfParams?: number parameterStructures?: unknown } export interface RequestProtocolSignature0 { method: string } export interface RequestSignature { method: string numberOfParams?: number parameterStructures?: unknown } export interface RequestSignature0 { method: string } export interface NotificationProtocolSignature { method: string numberOfParams?: number parameterStructures?: unknown } export interface NotificationProtocolSignature0 { readonly ____: [RO, _EM] | undefined method: string } export interface NotificationSignature

{ readonly _: [P, _EM] | undefined method: string numberOfParams?: number parameterStructures?: unknown } export interface NotificationSignature0 { method: string } export class ProtocolRequestType0 extends RequestType0 implements ProgressType, RegistrationType { /** * Clients must not use these properties. They are here to ensure correct typing. * in TypeScript */ readonly ___: [PR, RO, _EM] | undefined readonly ____: [RO, _EM] | undefined readonly _pr: PR | undefined constructor(method: string) } export class ProtocolRequestType extends RequestType implements ProgressType, RegistrationType { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly ___: [PR, RO, _EM] | undefined readonly ____: [RO, _EM] | undefined readonly _pr: PR | undefined constructor(method: string) } export class ProtocolNotificationType0 extends NotificationType0 implements RegistrationType { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly ___: [RO, _EM] | undefined readonly ____: [RO, _EM] | undefined constructor(method: string) } export class ProtocolNotificationType extends NotificationType

implements RegistrationType { /** * Clients must not use this property. It is here to ensure correct typing. */ readonly ___: [RO, _EM] | undefined readonly ____: [RO, _EM] | undefined constructor(method: string) } export interface NotificationHandler0 { (): void } export interface NotificationHandler

{ (params: P): void } /** * Including the registration options from languageserver protocol package could be too complicated * and the options can be changed from time to time. */ export interface GeneralRegistrationOptions { [key: string]: any } export interface DidChangeWatchedFilesRegistrationOptions { /** * The watchers to register. */ watchers: FileSystemWatcher[] } export interface DidChangeConfigurationRegistrationOptions { section?: string | string[] } interface TextDocumentRegistrationOptions { /** * A document selector to identify the scope of the registration. If set to null * the document selector provided on the client side will be used. */ documentSelector: DocumentSelector | null } interface TextDocumentChangeRegistrationOptions { /** * How documents are synced to the server. */ syncKind: 0 | 1 | 2 } interface TextDocumentSendFeature { /** * Returns a provider for the given text document. */ getProvider(document: TextDocument): { send: T } | undefined } interface NotificationSendEvent { original: E type: ProtocolNotificationType params: P } interface DidOpenTextDocumentParams { /** * The document that was opened. */ textDocument: TextDocumentItem } interface NotifyingFeature { onNotificationSent: Event> } export interface DidOpenTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(textDocument: TextDocument) => Promise>, NotifyingFeature { openDocuments: Iterable } export interface DidChangeTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(event: DidChangeTextDocumentParams) => Promise>, NotifyingFeature> { } export interface DidSaveTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(textDocument: TextDocument) => Promise>, NotifyingFeature {} export interface DidCloseTextDocumentFeatureShape extends DynamicFeature, TextDocumentSendFeature<(textDocument: TextDocument) => Promise>, NotifyingFeature {} export interface TextDocumentContentProviderShape { scheme: string onDidChangeEmitter: Emitter provider: TextDocumentContentProvider } export interface WorkspaceProviderFeature { getProviders(): PR[] | undefined } export interface TextDocumentProviderFeature { readonly registrationLength: number /** * Triggers the corresponding RPC method. */ getProvider(textDocument: TextDocument): T | undefined } export interface CodeLensProviderShape { provider?: CodeLensProvider onDidChangeCodeLensEmitter: Emitter } export interface SemanticTokensProviderShape { range?: DocumentRangeSemanticTokensProvider full?: DocumentSemanticTokensProvider onDidChangeSemanticTokensEmitter: Emitter } export interface InlineValueProviderShape { provider: InlineValuesProvider onDidChangeInlineValues: Emitter } export interface InlayHintsProviderShape { provider: InlayHintsProvider onDidChangeInlayHints: Emitter } export interface FoldingRangeProviderShape { provider: FoldingRangeProvider onDidChangeFoldingRange: Emitter } export interface DiagnosticProviderShape { /** * An event that signals that the diagnostics should be refreshed for * all documents. */ onDidChangeDiagnosticsEmitter: Emitter /** * The provider of diagnostics. */ diagnostics: DiagnosticProvider /** * Forget the given document and remove all diagnostics. * * @param document The document to forget. */ forget(document: TextDocument): void } export interface DiagnosticFeatureShape { refresh(): void } /** * A language server for manage a language server. * It's recommended to use `services.registerLanguageClient` to register language client to serviers, * you can have language client listed in `CocList services` and services could start the language client * by `documentselector` of `clientOptions`. */ export class LanguageClient { readonly id: string readonly name: string constructor(id: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean) /** * Create language client by name and options, don't forget to register language client * to services by `services.registerLanguageClient` */ constructor(name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean) sendRequest(type: ProtocolRequestType0 | RequestProtocolSignature0, token?: CancellationToken): Promise sendRequest(type: ProtocolRequestType | RequestProtocolSignature, params: P, token?: CancellationToken): Promise sendRequest(type: RequestType0 | RequestSignature0, token?: CancellationToken): Promise sendRequest(type: RequestType | RequestSignature, params: P, token?: CancellationToken): Promise sendRequest(method: string, token?: CancellationToken): Promise sendRequest(method: string, param: any, token?: CancellationToken): Promise onRequest(type: ProtocolRequestType0, handler: RequestHandler0): Disposable onRequest(type: ProtocolRequestType, handler: RequestHandler): Disposable onRequest(type: RequestType0, handler: RequestHandler0): Disposable onRequest(type: RequestType, handler: RequestHandler): Disposable onRequest(method: string, handler: (...params: any[]) => HandlerResult): Disposable sendNotification(type: ProtocolNotificationType0 | NotificationProtocolSignature0): Promise sendNotification(type: ProtocolNotificationType | NotificationSignature

, params?: P): Promise sendNotification(type: NotificationType0 | NotificationSignature0): Promise sendNotification

(type: NotificationType

| NotificationSignature

, params?: P): Promise sendNotification(method: string): Promise sendNotification(method: string, params: any): Promise onNotification(type: ProtocolNotificationType0, handler: NotificationHandler0): Disposable onNotification(type: ProtocolNotificationType, handler: NotificationHandler

): Disposable onNotification(type: NotificationType0, handler: () => void): Disposable onNotification

(type: NotificationType

, handler: (params: P) => void): Disposable onNotification(method: string, handler: (...params: any[]) => void): Disposable onProgress

(type: ProgressType, token: string | number, handler: (params: P) => void): Disposable sendProgress

(type: ProgressType

, token: string | number, value: P): Promise /** * Append debug message to outputChannel */ debug(message: string, data?: any, showNotification?: boolean): void /** * Append info message to outputChannel */ info(message: string, data?: any, showNotification?: boolean): void /** * Append warning message to outputChannel */ warn(message: string, data?: any, showNotification?: boolean): void /** * Append error message to outputChannel */ error(message: string, data?: any, showNotification?: boolean | 'force'): void /** * Append trace message to traceOutputChannel or outputChannel */ traceMessage(message: string, data?: any): void readonly state: State readonly middleware: Middleware readonly initializeResult: InitializeResult | undefined readonly clientOptions: LanguageClientOptions readonly outputChannel: OutputChannel /** * Fired on language server state change. */ readonly onDidChangeState: Event readonly diagnostics: DiagnosticCollection | undefined /** * Current running state. */ readonly serviceState: ServiceStat readonly started: boolean /** * The server is running in debug mode by forceDebug or debug arguments of NodeJS. */ readonly isInDebugMode: boolean /** * Check if server could start. */ needsStart(): boolean /** * Check if server could stop. */ needsStop(): boolean /** * Resolved when server ready */ onReady(): Promise set trace(value: Trace) /** * Return true when the client is running. */ isRunning(): boolean /** * Stop language server. */ stop(): Promise /** * Start language server, not needed when registered to services by `services.registerLanguageClient` */ start(): Promise /** * Restart language client. */ restart(): Promise dispose(): Promise /** * Register custom feature. */ registerFeature(feature: StaticFeature | DynamicFeature): void /** * Log failed request to outputChannel and throw error when necessary. * @param type The request type. * @param token CancellationToken used for request. * @param error Request error. * @param defaultValue Default return value when request cancelled or * connection got disposed. * @param showNotification Show message notification, default to true. */ handleFailedRequest(type: P, token: CancellationToken | undefined, error: any, defaultValue: T, showNotification?: boolean): T /** * Create a default error handler. */ createDefaultErrorHandler(maxRestartCount?: number): ErrorHandler getFeature(request: 'workspace/executeCommand'): DynamicFeature getFeature(request: 'workspace/didChangeWorkspaceFolders'): DynamicFeature getFeature(request: 'workspace/didChangeWatchedFiles'): DynamicFeature getFeature(request: 'workspace/didChangeConfiguration'): DynamicFeature getFeature(request: 'textDocument/didOpen'): DidOpenTextDocumentFeatureShape getFeature(request: 'textDocument/didChange'): DidChangeTextDocumentFeatureShape getFeature(request: 'textDocument/willSave'): DynamicFeature & TextDocumentSendFeature<(textDocument: TextDocumentWillSaveEvent) => Promise> getFeature(request: 'textDocument/willSaveWaitUntil'): DynamicFeature & TextDocumentSendFeature<(textDocument: TextDocument) => ProviderResult> getFeature(request: 'textDocument/didSave'): DidSaveTextDocumentFeatureShape getFeature(request: 'textDocument/didClose'): DidCloseTextDocumentFeatureShape getFeature(request: 'workspace/didCreateFiles'): DynamicFeature & { send: (event: FileCreateEvent) => Promise } getFeature(request: 'workspace/didRenameFiles'): DynamicFeature & { send: (event: FileRenameEvent) => Promise } getFeature(request: 'workspace/didDeleteFiles'): DynamicFeature & { send: (event: FileDeleteEvent) => Promise } getFeature(request: 'workspace/willCreateFiles'): DynamicFeature & { send: (event: FileWillCreateEvent) => Promise } getFeature(request: 'workspace/willRenameFiles'): DynamicFeature & { send: (event: FileWillRenameEvent) => Promise } getFeature(request: 'workspace/willDeleteFiles'): DynamicFeature & { send: (event: FileWillDeleteEvent) => Promise } getFeature(request: 'workspace/symbol'): DynamicFeature & WorkspaceProviderFeature getFeature(request: 'textDocument/completion'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/hover'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/signatureHelp'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/definition'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/references'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/documentHighlight'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/codeAction'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/codeLens'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/formatting'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/rangeFormatting'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/onTypeFormatting'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/rename'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/documentSymbol'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/documentLink'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/documentColor'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/declaration'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/foldingRange'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/implementation'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/selectionRange'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/typeDefinition'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/prepareCallHierarchy'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/semanticTokens'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/linkedEditingRange'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/prepareTypeHierarchy'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/inlineValue'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/inlayHint'): DynamicFeature & TextDocumentProviderFeature getFeature(request: 'textDocument/diagnostic'): DynamicFeature & TextDocumentProviderFeature & DiagnosticFeatureShape getFeature(request: 'workspace/textDocumentContent'): DynamicFeature & WorkspaceProviderFeature getFeature(request: 'textDocument/inlineCompletion'): DynamicFeature & TextDocumentProviderFeature } /** * Monitor for setting change, restart language server when specified setting changed. */ export class SettingMonitor { constructor(client: LanguageClient, setting: string) start(): Disposable } // }} } // vim: set tw=80 sw=2 ts=2 sts=2 et foldmarker={{,}} foldmethod=marker foldlevel=0 nofen: