mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	fix(lsp): refactor escaping snippet text (#25611)
This commit is contained in:
		 Maria José Solano
					Maria José Solano
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							8ee8112b92
						
					
				
				
					commit
					ee156ca60e
				
			| @@ -20,11 +20,15 @@ local format_capture = Cg(int / tonumber, 'capture') | ||||
| local format_modifier = Cg(P('upcase') + P('downcase') + P('capitalize'), 'modifier') | ||||
| local tabstop = Cg(int / tonumber, 'tabstop') | ||||
|  | ||||
| -- These characters are always escapable in text nodes no matter the context. | ||||
| local escapable = '$}\\' | ||||
|  | ||||
| --- Returns a function that unescapes occurrences of "special" characters. | ||||
| --- | ||||
| --- @param special string | ||||
| --- @param special? string | ||||
| --- @return fun(match: string): string | ||||
| local function escape_text(special) | ||||
|   special = special or escapable | ||||
|   return function(match) | ||||
|     local escaped = match:gsub('\\(.)', function(c) | ||||
|       return special:find(c) and c or '\\' .. c | ||||
| @@ -33,25 +37,33 @@ local function escape_text(special) | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Text nodes match "any character", but $, \, and } must be escaped. | ||||
| local escapable = '$}\\' | ||||
| local text = (backslash * S(escapable)) + (P(1) - S(escapable)) | ||||
| local text_0, text_1 = (text ^ 0) / escape_text(escapable), text ^ 1 | ||||
| --- Returns a pattern for text nodes. Will match characters in `escape` when preceded by a backslash, | ||||
| --- and will stop with characters in `stop_with`. | ||||
| --- | ||||
| --- @param escape string | ||||
| --- @param stop_with? string | ||||
| --- @return vim.lpeg.Pattern | ||||
| local function text(escape, stop_with) | ||||
|   stop_with = stop_with or escape | ||||
|   return (backslash * S(escape)) + (P(1) - S(stop_with)) | ||||
| end | ||||
|  | ||||
| -- For text nodes inside curly braces. It stops parsing when reaching an escapable character. | ||||
| local braced_text = (text(escapable) ^ 0) / escape_text() | ||||
|  | ||||
| -- Within choice nodes, \ also escapes comma and pipe characters. | ||||
| local choice_text = C(((backslash * S(escapable .. ',|')) + (P(1) - S(escapable .. ',|'))) ^ 1) | ||||
|   / escape_text(escapable .. ',|') | ||||
| local if_text, else_text = Cg(text_0, 'if_text'), Cg(text_0, 'else_text') | ||||
| local choice_text = C(text(escapable .. ',|') ^ 1) / escape_text(escapable .. ',|') | ||||
|  | ||||
| -- Within format nodes, make sure we stop at / | ||||
| local format_text = C(((backslash * S(escapable)) + (P(1) - S(escapable .. '/'))) ^ 1) | ||||
|   / escape_text(escapable) | ||||
| local format_text = C(text(escapable, escapable .. '/') ^ 1) / escape_text() | ||||
|  | ||||
| local if_text, else_text = Cg(braced_text, 'if_text'), Cg(braced_text, 'else_text') | ||||
|  | ||||
| -- Within ternary condition format nodes, make sure we stop at : | ||||
| local if_till_colon_text = Cg( | ||||
|   C(((backslash * S(escapable)) + (P(1) - S(escapable .. ':'))) ^ 1) / escape_text(escapable), | ||||
|   'if_text' | ||||
| ) | ||||
| local if_till_colon_text = Cg(C(text(escapable, escapable .. ':') ^ 1) / escape_text(), 'if_text') | ||||
|  | ||||
| -- Matches the string inside //, allowing escaping of the closing slash. | ||||
| local regex = Cg(((backslash * slash) + (P(1) - slash)) ^ 1, 'regex') | ||||
| local regex = Cg(text('/') ^ 1, 'regex') | ||||
|  | ||||
| -- Regex constructor flags (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#parameters). | ||||
| local options = Cg(S('dgimsuvy') ^ 0, 'options') | ||||
| @@ -94,11 +106,11 @@ local G = P({ | ||||
|   snippet = Ct(Cg( | ||||
|     Ct(( | ||||
|       V('any') + | ||||
|       (Ct(Cg(text_1 / escape_text(escapable), 'text')) / node(Type.Text)) | ||||
|       (Ct(Cg((text(escapable, '$') ^ 1) / escape_text(), 'text')) / node(Type.Text)) | ||||
|     ) ^ 1), 'children' | ||||
|   )) / node(Type.Snippet), | ||||
|   any_or_text = V('any') + (Ct(Cg(text_0 / escape_text(escapable), 'text')) / node(Type.Text)), | ||||
|   ) * -P(1)) / node(Type.Snippet), | ||||
|   any = V('placeholder') + V('tabstop') + V('choice') + V('variable'), | ||||
|   any_or_text = V('any') + (Ct(Cg(braced_text, 'text')) / node(Type.Text)), | ||||
|   tabstop = Ct(dollar * (tabstop + (l_brace * tabstop * r_brace))) / node(Type.Tabstop), | ||||
|   placeholder = Ct(dollar * l_brace * tabstop * colon * Cg(V('any_or_text'), 'value') * r_brace) / node(Type.Placeholder), | ||||
|   choice = Ct(dollar * | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| local helpers = require('test.functional.helpers')(after_each) | ||||
| local snippet = require('vim.lsp._snippet_grammar') | ||||
| local type = snippet.NodeType | ||||
|  | ||||
| local eq = helpers.eq | ||||
| local exec_lua = helpers.exec_lua | ||||
| @@ -15,28 +16,28 @@ describe('vim.lsp._snippet_grammar', function() | ||||
|  | ||||
|   it('parses only text', function() | ||||
|     eq({ | ||||
|       { type = snippet.NodeType.Text, data = { text = 'TE$}XT' } }, | ||||
|       { type = type.Text, data = { text = 'TE$}XT' } }, | ||||
|     }, parse('TE\\$\\}XT')) | ||||
|   end) | ||||
|  | ||||
|   it('parses tabstops', function() | ||||
|     eq({ | ||||
|       { type = snippet.NodeType.Tabstop, data = { tabstop = 1 } }, | ||||
|       { type = snippet.NodeType.Tabstop, data = { tabstop = 2 } }, | ||||
|       { type = type.Tabstop, data = { tabstop = 1 } }, | ||||
|       { type = type.Tabstop, data = { tabstop = 2 } }, | ||||
|     }, parse('$1${2}')) | ||||
|   end) | ||||
|  | ||||
|   it('parses nested placeholders', function() | ||||
|     eq({ | ||||
|       { | ||||
|         type = snippet.NodeType.Placeholder, | ||||
|         type = type.Placeholder, | ||||
|         data = { | ||||
|           tabstop = 1, | ||||
|           value = { | ||||
|             type = snippet.NodeType.Placeholder, | ||||
|             type = type.Placeholder, | ||||
|             data = { | ||||
|               tabstop = 2, | ||||
|               value = { type = snippet.NodeType.Tabstop, data = { tabstop = 3 } }, | ||||
|               value = { type = type.Tabstop, data = { tabstop = 3 } }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
| @@ -46,24 +47,24 @@ describe('vim.lsp._snippet_grammar', function() | ||||
|  | ||||
|   it('parses variables', function() | ||||
|     eq({ | ||||
|       { type = snippet.NodeType.Variable, data = { name = 'VAR' } }, | ||||
|       { type = snippet.NodeType.Variable, data = { name = 'VAR' } }, | ||||
|       { type = type.Variable, data = { name = 'VAR' } }, | ||||
|       { type = type.Variable, data = { name = 'VAR' } }, | ||||
|       { | ||||
|         type = snippet.NodeType.Variable, | ||||
|         type = type.Variable, | ||||
|         data = { | ||||
|           name = 'VAR', | ||||
|           default = { type = snippet.NodeType.Tabstop, data = { tabstop = 1 } }, | ||||
|           default = { type = type.Tabstop, data = { tabstop = 1 } }, | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type = snippet.NodeType.Variable, | ||||
|         type = type.Variable, | ||||
|         data = { | ||||
|           name = 'VAR', | ||||
|           regex = 'regex', | ||||
|           options = '', | ||||
|           format = { | ||||
|             { | ||||
|               type = snippet.NodeType.Format, | ||||
|               type = type.Format, | ||||
|               data = { capture = 1, modifier = 'upcase' }, | ||||
|             }, | ||||
|           }, | ||||
| @@ -75,7 +76,7 @@ describe('vim.lsp._snippet_grammar', function() | ||||
|   it('parses choice', function() | ||||
|     eq({ | ||||
|       { | ||||
|         type = snippet.NodeType.Choice, | ||||
|         type = type.Choice, | ||||
|         data = { tabstop = 1, values = { ',', '|' } }, | ||||
|       }, | ||||
|     }, parse('${1|\\,,\\||}')) | ||||
| @@ -85,30 +86,30 @@ describe('vim.lsp._snippet_grammar', function() | ||||
|     eq( | ||||
|       { | ||||
|         { | ||||
|           type = snippet.NodeType.Variable, | ||||
|           type = type.Variable, | ||||
|           data = { | ||||
|             name = 'VAR', | ||||
|             regex = 'regex', | ||||
|             options = '', | ||||
|             format = { | ||||
|               { | ||||
|                 type = snippet.NodeType.Format, | ||||
|                 type = type.Format, | ||||
|                 data = { capture = 1, modifier = 'upcase' }, | ||||
|               }, | ||||
|               { | ||||
|                 type = snippet.NodeType.Format, | ||||
|                 type = type.Format, | ||||
|                 data = { capture = 1, if_text = 'if_text' }, | ||||
|               }, | ||||
|               { | ||||
|                 type = snippet.NodeType.Format, | ||||
|                 type = type.Format, | ||||
|                 data = { capture = 1, else_text = 'else_text' }, | ||||
|               }, | ||||
|               { | ||||
|                 type = snippet.NodeType.Format, | ||||
|                 type = type.Format, | ||||
|                 data = { capture = 1, if_text = 'if_text', else_text = 'else_text' }, | ||||
|               }, | ||||
|               { | ||||
|                 type = snippet.NodeType.Format, | ||||
|                 type = type.Format, | ||||
|                 data = { capture = 1, else_text = 'else_text' }, | ||||
|               }, | ||||
|             }, | ||||
| @@ -124,24 +125,24 @@ describe('vim.lsp._snippet_grammar', function() | ||||
|   it('parses empty strings', function() | ||||
|     eq({ | ||||
|       { | ||||
|         type = snippet.NodeType.Placeholder, | ||||
|         type = type.Placeholder, | ||||
|         data = { | ||||
|           tabstop = 1, | ||||
|           value = { type = snippet.NodeType.Text, data = { text = '' } }, | ||||
|           value = { type = type.Text, data = { text = '' } }, | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type = snippet.NodeType.Text, | ||||
|         type = type.Text, | ||||
|         data = { text = ' ' }, | ||||
|       }, | ||||
|       { | ||||
|         type = snippet.NodeType.Variable, | ||||
|         type = type.Variable, | ||||
|         data = { | ||||
|           name = 'VAR', | ||||
|           regex = 'erg', | ||||
|           format = { | ||||
|             { | ||||
|               type = snippet.NodeType.Format, | ||||
|               type = type.Format, | ||||
|               data = { capture = 1, if_text = '' }, | ||||
|             }, | ||||
|           }, | ||||
| @@ -150,4 +151,21 @@ describe('vim.lsp._snippet_grammar', function() | ||||
|       }, | ||||
|     }, parse('${1:} ${VAR/erg/${1:+}/g}')) | ||||
|   end) | ||||
|  | ||||
|   it('parses closing curly brace as text', function() | ||||
|     eq( | ||||
|       { | ||||
|         { type = type.Text, data = { text = 'function ' } }, | ||||
|         { type = type.Tabstop, data = { tabstop = 1 } }, | ||||
|         { type = type.Text, data = { text = '() {\n  ' } }, | ||||
|         { type = type.Tabstop, data = { tabstop = 0 } }, | ||||
|         { type = type.Text, data = { text = '\n}' } }, | ||||
|       }, | ||||
|       parse(table.concat({ | ||||
|         'function $1() {', | ||||
|         '  $0', | ||||
|         '}', | ||||
|       }, '\n')) | ||||
|     ) | ||||
|   end) | ||||
| end) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user