Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: snippet completions for functions #116

Merged
merged 4 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
ignore = {
"631", -- max_line_length
}
exclude_files = {
".tests",
}
globals = { "vim", "P" }
read_globals = {
"describe",
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ require("typescript-tools").setup {
-- described below
tsserver_format_options = {},
tsserver_file_preferences = {},
-- mirror of VSCode's `typescript.suggest.completeFunctionCalls`
complete_function_calls = false,
},
}
```
Expand Down
69 changes: 69 additions & 0 deletions lua/typescript-tools/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,61 @@
---@field tsserver_format_options table|fun(filetype: string): table
---@field tsserver_file_preferences table|fun(filetype: string): table
---@field tsserver_max_memory number|"auto"
---@field complete_function_calls boolean
---@field expose_as_code_action ("fix_all"| "add_missing_imports"| "remove_unused")[]
local M = {}
local __store = {}

-- INFO: this two defaults are same as in vscode
local default_format_options = {
insertSpaceAfterCommaDelimiter = true,
insertSpaceAfterConstructor = false,
insertSpaceAfterSemicolonInForStatements = true,
insertSpaceBeforeAndAfterBinaryOperators = true,
insertSpaceAfterKeywordsInControlFlowStatements = true,
insertSpaceAfterFunctionKeywordForAnonymousFunctions = true,
insertSpaceBeforeFunctionParenthesis = false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis = false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets = false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces = true,
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces = true,
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces = false,
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces = false,
insertSpaceAfterTypeAssertion = false,
placeOpenBraceOnNewLineForFunctions = false,
placeOpenBraceOnNewLineForControlBlocks = false,
semicolons = "ignore",
indentSwitchCase = true,
}

local default_preferences = {
quotePreference = "auto",
importModuleSpecifierEnding = "auto",
jsxAttributeCompletionStyle = "auto",
allowTextChangesInNewFiles = true,
providePrefixAndSuffixTextForRename = true,
allowRenameOfImportPath = true,
includeAutomaticOptionalChainCompletions = true,
provideRefactorNotApplicableReason = true,
generateReturnInDocTemplate = true,
includeCompletionsForImportStatements = true,
includeCompletionsWithSnippetText = true,
includeCompletionsWithClassMemberSnippets = true,
includeCompletionsWithObjectLiteralMethodSnippets = true,
useLabelDetailsInCompletionEntries = true,
allowIncompleteCompletions = true,
displayPartsForJSDoc = true,
disableLineTextInReferences = true,
includeInlayParameterNameHints = "none",
includeInlayParameterNameHintsWhenArgumentMatchesName = false,
includeInlayFunctionParameterTypeHints = false,
includeInlayVariableTypeHints = false,
includeInlayVariableTypeHintsWhenTypeMatchesName = false,
includeInlayPropertyDeclarationTypeHints = false,
includeInlayFunctionLikeReturnTypeHints = false,
includeInlayEnumMemberValueHints = false,
}

---@enum tsserver_log_level
M.tsserver_log_level = {
normal = "normal",
Expand Down Expand Up @@ -56,6 +107,7 @@ function M.load_settings(settings)
{ "number", "string" },
true,
},
["settings.complete_function_calls"] = { settings.complete_function_calls, "boolean", true },
["settings.expose_as_code_action"] = {
settings.expose_as_code_action,
"table",
Expand Down Expand Up @@ -93,6 +145,10 @@ function M.load_settings(settings)
__store.tsserver_max_memory = "auto"
end

if not settings.complete_function_calls then
__store.complete_function_calls = false
end

if not settings.expose_as_code_action then
__store.expose_as_code_action = {}
end
Expand All @@ -104,4 +160,17 @@ setmetatable(M, {
end,
})

---@param filetype vim.opt.filetype
---@return table
function M.get_tsserver_file_preferences(filetype)
local preferences = __store.tsserver_file_preferences
return vim.tbl_extend(
"force",
default_preferences,
type(preferences) == "function" and preferences(filetype) or preferences
)
end

M.default_format_options = default_format_options

return M
1 change: 1 addition & 0 deletions lua/typescript-tools/protocol/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ return {
Imports = "imports",
Region = "region",
},
---@enum InsertTextFormat
InsertTextFormat = {
PlainText = 1,
Snippet = 2,
Expand Down
11 changes: 9 additions & 2 deletions lua/typescript-tools/protocol/text_document/completion/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function M.handler(request, response, params)
local text_document = params.textDocument
local context = params.context or {}
local trigger_character = context.triggerCharacter
local requested_bufnr = vim.uri_to_bufnr(text_document.uri)
local filetype = vim.bo[requested_bufnr].filetype

-- tsserver protocol reference:
-- https//github.com/microsoft/TypeScript/blob/8b482b513d87c6fcda8ece18b99f8a01cff5c605/lib/protocol.d.ts#L1631
Expand Down Expand Up @@ -65,14 +67,19 @@ function M.handler(request, response, params)
sortText = "\u{ffff}" .. item.sortText
end

local should_create_function_snippet = utils.should_create_function_snippet(kind, filetype)
local should_create_snippet = item.isSnippet or should_create_function_snippet
local label = is_optional and (item.name .. "?") or item.name
label = should_create_function_snippet and (label .. "(...)") or label
pmizio marked this conversation as resolved.
Show resolved Hide resolved

return {
label = is_optional and (item.name .. "?") or item.name,
label = label,
labelDetails = item.labelDetails,
insertText = insertText,
filterText = insertText,
commitCharacters = item_kind_utils.calculate_commit_characters(kind),
kind = kind,
insertTextFormat = item.isSnippet and c.InsertTextFormat.Snippet
insertTextFormat = should_create_snippet and c.InsertTextFormat.Snippet
or c.InsertTextFormat.PlainText,
sortText = sortText,
textEdit = calculate_text_edit(item.replacementSpan, insertText),
Expand Down
96 changes: 94 additions & 2 deletions lua/typescript-tools/protocol/text_document/completion/resolve.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,89 @@ local function make_text_edits(code_actions)
return text_edits
end

---@alias DisplayPart { kind: string, text: string }

---@param display_parts DisplayPart[]
---@return { has_optional_parameters: boolean, parts: DisplayPart[] }
---@see https://github.com/typescript-language-server/typescript-language-server/blob/983a6923114c39d638e0c7d419ae16e8bca8985c/src/completion.ts#L355-L371
local function get_parameter_list_parts(display_parts)
local parts = {}
local is_in_method = false
local has_optional_parameters = false
local paren_count = 0
local brace_count = 0

for i, part in ipairs(display_parts) do
if
part.kind == "methodName"
or part.kind == "functionName"
or part.kind == "text"
or part.kind == "propertyName"
then
if paren_count == 0 and brace_count == 0 then
is_in_method = true
end
elseif part.kind == "parameterName" then
if paren_count == 1 and brace_count == 0 and is_in_method then
local next = display_parts[i + 1]
local name_is_followed_by_optional_indicator = next and next.text == "?"
local name_is_this = part.text == "this"
if not name_is_followed_by_optional_indicator and not name_is_this then
table.insert(parts, part)
end
has_optional_parameters = has_optional_parameters or name_is_followed_by_optional_indicator
end
elseif part.kind == "punctuation" then
if part.text == "(" then
paren_count = paren_count + 1
elseif part.text == ")" then
paren_count = paren_count - 1
if paren_count <= 0 and is_in_method then
break
end
elseif part.text == "..." and paren_count == 1 then
has_optional_parameters = true
break
elseif part.text == "{" then
brace_count = brace_count + 1
elseif part.text == "}" then
brace_count = brace_count - 1
end
end
end
return { has_optional_parameters = has_optional_parameters, parts = parts }
end

---@alias PartialCompletionItem
---| { insertText: string, insertTextFormat: InsertTextFormat, textEdit: { newText: string }, label: string }

---@param item PartialCompletionItem
---@param display_parts DisplayPart[]
---@return nil
---@see https://github.com/typescript-language-server/typescript-language-server/blob/983a6923114c39d638e0c7d419ae16e8bca8985c/src/completion.ts#L355-L371
local function create_snippet(item, display_parts)
local parameter_list_parts = get_parameter_list_parts(display_parts)
local has_optional_parameters = parameter_list_parts.has_optional_parameters
local parts = parameter_list_parts.parts
local snippet =
string.format("%s(", item.insertText or (item.textEdit and item.textEdit.newText) or item.label)
for i, part in ipairs(parts) do
snippet = snippet .. string.format("${%d:%s}", i, part.text:gsub("([$}\\])", "\\%1"))
if i ~= #parts then
snippet = snippet .. ", "
end
end
if has_optional_parameters then
snippet = snippet .. string.format("$%d", #parts + 1)
end
snippet = snippet .. ")$0"
item.insertText = snippet
item.insertTextFormat = c.InsertTextFormat.Snippet
if item.textEdit then
item.textEdit.newText = snippet
end
end

---@param params table
---@return table
local function completion_resolve_request(params)
Expand All @@ -50,6 +133,9 @@ end

---@type TsserverProtocolHandler
function M.handler(request, response, params)
local requested_bufnr = vim.uri_to_bufnr(vim.uri_from_fname(params.data.file))
local filetype = vim.bo[requested_bufnr].filetype

-- tsserver protocol reference:
-- https://github.com/microsoft/TypeScript/blob/549e61d0af1ba885be29d69f341e7d3a00686071/lib/protocol.d.ts#L1661
request(completion_resolve_request(params))
Expand All @@ -74,7 +160,7 @@ function M.handler(request, response, params)
detail = "Auto import from " .. utils.tsserver_docs_to_plain_text(source) .. "\n" .. detail
end

response(vim.tbl_extend("force", params, {
local item = vim.tbl_extend("force", params, {
detail = detail,
documentation = {
kind = c.MarkupKind.Markdown,
Expand All @@ -83,7 +169,13 @@ function M.handler(request, response, params)
additionalTextEdits = make_text_edits(details.codeActions),
-- INFO: there is also `command` prop but I don't know there is usecase for that here,
-- or neovim even handle that for now i skip this
}))
})

if utils.should_create_function_snippet(item.kind, filetype) then
create_snippet(item, details.displayParts)
end

response(item)
else
response(nil)
end
Expand Down
59 changes: 2 additions & 57 deletions lua/typescript-tools/protocol/text_document/did_open.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,6 @@ local plugin_config = require "typescript-tools.config"

local M = {}

-- INFO: this two defaults are same as in vscode
local default_format_options = {
insertSpaceAfterCommaDelimiter = true,
insertSpaceAfterConstructor = false,
insertSpaceAfterSemicolonInForStatements = true,
insertSpaceBeforeAndAfterBinaryOperators = true,
insertSpaceAfterKeywordsInControlFlowStatements = true,
insertSpaceAfterFunctionKeywordForAnonymousFunctions = true,
insertSpaceBeforeFunctionParenthesis = false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis = false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets = false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces = true,
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces = true,
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces = false,
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces = false,
insertSpaceAfterTypeAssertion = false,
placeOpenBraceOnNewLineForFunctions = false,
placeOpenBraceOnNewLineForControlBlocks = false,
semicolons = "ignore",
indentSwitchCase = true,
}

local default_preferences = {
quotePreference = "auto",
importModuleSpecifierEnding = "auto",
jsxAttributeCompletionStyle = "auto",
allowTextChangesInNewFiles = true,
providePrefixAndSuffixTextForRename = true,
allowRenameOfImportPath = true,
includeAutomaticOptionalChainCompletions = true,
provideRefactorNotApplicableReason = true,
generateReturnInDocTemplate = true,
includeCompletionsForImportStatements = true,
includeCompletionsWithSnippetText = true,
includeCompletionsWithClassMemberSnippets = true,
includeCompletionsWithObjectLiteralMethodSnippets = true,
useLabelDetailsInCompletionEntries = true,
allowIncompleteCompletions = true,
displayPartsForJSDoc = true,
disableLineTextInReferences = true,
includeInlayParameterNameHints = "none",
includeInlayParameterNameHintsWhenArgumentMatchesName = false,
includeInlayFunctionParameterTypeHints = false,
includeInlayVariableTypeHints = false,
includeInlayVariableTypeHintsWhenTypeMatchesName = false,
includeInlayPropertyDeclarationTypeHints = false,
includeInlayFunctionLikeReturnTypeHints = false,
includeInlayEnumMemberValueHints = false,
}

---@type table<"mac" | "unix" | "dos", string>
local eol_chars = {
mac = "\r",
Expand All @@ -78,7 +28,6 @@ local function configure(params)
local convert_tabs_to_spaces = bo.expandtab or true
local new_line_character = get_eol_chars(bo)

local preferences = plugin_config.tsserver_file_preferences
local format_options = plugin_config.tsserver_format_options

return {
Expand All @@ -93,14 +42,10 @@ local function configure(params)
convertTabsToSpaces = convert_tabs_to_spaces,
newLineCharacter = new_line_character,
},
default_format_options,
plugin_config.default_format_options,
type(format_options) == "function" and format_options(bo.filetype) or format_options
),
preferences = vim.tbl_extend(
"force",
default_preferences,
type(preferences) == "function" and preferences(bo.filetype) or preferences
),
preferences = plugin_config.get_tsserver_file_preferences(bo.filetype),
},
}
end
Expand Down
3 changes: 1 addition & 2 deletions lua/typescript-tools/protocol/text_document/inlay_hint.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ local inlay_hint_kind_map = {
---@param filetype string
---@return boolean
local function are_inlay_hints_enabled(filetype)
local preferences = plugin_config.tsserver_file_preferences
preferences = type(preferences) == "function" and preferences(filetype) or preferences
local preferences = plugin_config.get_tsserver_file_preferences(filetype)

if not preferences then
return false
Expand Down
12 changes: 12 additions & 0 deletions lua/typescript-tools/protocol/utils.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local c = require "typescript-tools.protocol.constants"
local plugin_config = require "typescript-tools.config"

local M = {}

Expand Down Expand Up @@ -222,4 +223,15 @@ function M.cancelled_response(data)
}
end

---@param kind CompletionItemKind
---@param filetype vim.opt.filetype
---@return boolean
---@see https://github.com/typescript-language-server/typescript-language-server/blob/983a6923114c39d638e0c7d419ae16e8bca8985c/src/completion.ts#L355-L371
function M.should_create_function_snippet(kind, filetype)
local preferences = plugin_config.get_tsserver_file_preferences(filetype)
return preferences.includeCompletionsWithSnippetText
and (kind == c.CompletionItemKind.Function or kind == c.CompletionItemKind.Method)
and plugin_config.complete_function_calls
end

return M
Loading