Skip to content

Commit

Permalink
feat: snippet completions for functions (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
llllvvuu authored Jul 27, 2023
1 parent a616eb8 commit bdc5878
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 69 deletions.
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

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

0 comments on commit bdc5878

Please sign in to comment.