Skip to content

Commit 51a61ab

Browse files
committed
feat: Adds html support
1 parent 457dc4d commit 51a61ab

File tree

4 files changed

+263
-1
lines changed

4 files changed

+263
-1
lines changed

lua/typescript-tools/autocommands/diagnostics.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ local proto_utils = require "typescript-tools.protocol.utils"
88

99
local publish_diagnostic_mode = plugin_config.publish_diagnostic_mode
1010

11-
local extensions_pattern = { "*.js", "*.mjs", "*.jsx", "*.ts", "*.tsx", "*.mts" }
11+
local extensions_pattern = { "*.js", "*.mjs", "*.jsx", "*.ts", "*.tsx", "*.mts", "*.html" }
1212

1313
local M = {}
1414

lua/typescript-tools/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ M.setup = function(config)
2323
"typescript",
2424
"typescriptreact",
2525
"typescript.tsx",
26+
"html",
2627
},
2728
root_dir = function(fname)
2829
-- INFO: stealed from:
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
local plugin_config = require "typescript-tools.config"
2+
3+
-- Basic idea is for HTML support is to whenever we edit a file we create an empty buffer
4+
-- and replace every character with " " - space. Then we use treesitter to get all of the script tags
5+
-- from the file and we insert them into the empty buffer in the same positions that they
6+
-- were in original buffer. Because of that we don't have to translate any positions anywhere.
7+
8+
local M = {}
9+
10+
local VIRTUAL_DOCUMENT_EXTENSION = ".js"
11+
12+
local SCRIPT_TEXT_HTML_QUERY = [[
13+
(script_element
14+
(start_tag)
15+
(raw_text) @script.text
16+
(end_tag))
17+
]]
18+
19+
local function is_position_between_range(position, range)
20+
local start_row, start_col, end_row, end_col = unpack(range)
21+
22+
return not (
23+
position.line < start_row
24+
or position.line > end_row
25+
or (position.line == start_row and position.character < start_col)
26+
or (position.line == end_row and position.character > end_col)
27+
)
28+
end
29+
30+
local function initialize_virtual_document()
31+
-- creating path in the same directory as edited file so every setting set up for directory will apply
32+
local virtual_document_path = vim.api.nvim_buf_get_name(0) .. "-tmp" .. VIRTUAL_DOCUMENT_EXTENSION
33+
M.virtual_document_uri = "file://" .. virtual_document_path
34+
-- uri_to_bufnr creates a buffer if it doesn't exist
35+
M.virtual_document_bufnr = vim.uri_to_bufnr(M.virtual_document_uri)
36+
37+
vim.api.nvim_buf_set_name(M.virtual_document_bufnr, virtual_document_path)
38+
vim.api.nvim_buf_set_option(M.virtual_document_bufnr, "swapfile", false)
39+
vim.api.nvim_buf_set_option(M.virtual_document_bufnr, "buftype", "nowrite")
40+
end
41+
42+
--- @param bufnr number - buffer number to extract nodes from
43+
--- @return table - list of script tag's texts nodes
44+
local function extract_script_text_nodes(bufnr)
45+
local ft = vim.api.nvim_buf_get_option(bufnr, "filetype")
46+
local parserlang = vim.treesitter.language.get_lang(ft)
47+
48+
if not parserlang then
49+
return {}
50+
end
51+
52+
local language_tree = vim.treesitter.get_parser(bufnr, parserlang)
53+
local syntax_tree = language_tree:parse()
54+
local root = syntax_tree[1]:root()
55+
56+
local query = vim.treesitter.query.parse(parserlang, SCRIPT_TEXT_HTML_QUERY)
57+
58+
local nodes = {}
59+
for _, match, _ in query:iter_matches(root, main_nr) do
60+
for id, node in pairs(match) do
61+
local name = query.captures[id]
62+
if name == "script.text" then
63+
table.insert(nodes, node)
64+
end
65+
end
66+
end
67+
68+
return nodes
69+
end
70+
71+
--- @return table - extracts code chunks from script tags including their ranges and texts
72+
local function extract_js_script_code_ranges()
73+
local script_nodes = extract_script_text_nodes(0)
74+
local code_chunks = {}
75+
for _, script_node in ipairs(script_nodes) do
76+
local text = vim.treesitter.get_node_text(script_node, 0)
77+
-- we are taking positions of start and end tags because (raw_text) does not include whitespace
78+
-- and we need to take range between the tags
79+
local script_start_tag_node = script_node:prev_sibling()
80+
local script_end_tag_node = script_node:next_sibling()
81+
82+
local _, _, start_row, start_col = script_start_tag_node:range()
83+
local end_row, end_col = script_end_tag_node:range()
84+
-- TS indexes rows from 0 and columns from 0. Nvim indexes rows from 1 and columns from 0.
85+
-- start_row + 1 because of indexing difference
86+
-- start_col + 1 because we want to take the first character after opening script tag
87+
-- end_row + 1 because of indexing difference
88+
table.insert(code_chunks, {
89+
range = { start_row + 1, start_col + 1, end_row + 1, end_col == 0 and 0 or end_col - 1 },
90+
})
91+
end
92+
93+
return code_chunks
94+
end
95+
96+
--- Gets the content from buffer, replaces everything with empty lines and then inserts code chunks
97+
--- at correct positions and replaces virtual document with those lines.
98+
--- @param original_buffer_uri string - uri of the buffer to extract code from
99+
function M.update_virtual_document(original_buffer_uri)
100+
if not M.virtual_document_bufnr then
101+
initialize_virtual_document()
102+
end
103+
104+
local requested_buf_all_lines =
105+
vim.api.nvim_buf_get_lines(vim.uri_to_bufnr(original_buffer_uri), 0, -1, false)
106+
107+
local scripts_ranges = extract_js_script_code_ranges()
108+
109+
local function replace_char(pos, str, r)
110+
return str:sub(1, pos - 1) .. r .. str:sub(pos + 1)
111+
end
112+
113+
-- this might be not that performant but we should observe how it performs
114+
for line_index, line in ipairs(requested_buf_all_lines) do
115+
for character_index = 1, #line do
116+
local is_position_in_script = false
117+
118+
for _, script_range in ipairs(scripts_ranges) do
119+
if
120+
is_position_between_range(
121+
{ line = line_index, character = character_index },
122+
script_range.range
123+
)
124+
then
125+
is_position_in_script = true
126+
break
127+
end
128+
end
129+
130+
if not is_position_in_script then
131+
requested_buf_all_lines[line_index] =
132+
replace_char(character_index, requested_buf_all_lines[line_index], " ")
133+
end
134+
end
135+
end
136+
137+
-- this line throws E565: Not allowed to change text or change window sometimes and nned to investigate why
138+
vim.api.nvim_buf_set_lines(M.virtual_document_bufnr, 0, -1, false, requested_buf_all_lines)
139+
140+
return M.virtual_document_bufnr
141+
end
142+
143+
function M.rewrite_request_uris(method, params, current_buffer_uri)
144+
if not current_buffer_uri then
145+
return params
146+
end
147+
148+
M.update_virtual_document(current_buffer_uri)
149+
150+
local function replace_original_uri_to_virtual_document_uri(tbl)
151+
for key, value in pairs(tbl) do
152+
if type(value) == "table" then
153+
replace_original_uri_to_virtual_document_uri(value) -- Recursive call for nested tables
154+
elseif type(value) == "string" and value == current_buffer_uri then
155+
tbl[key] = M.virtual_document_uri
156+
end
157+
end
158+
159+
-- in those methods there are whole contents of the file so we need to rewrite them as well
160+
if tbl.text and (method == "textDocument/didOpen") then
161+
tbl.text =
162+
table.concat(vim.api.nvim_buf_get_lines(M.virtual_document_bufnr, 0, -1, false), "\n")
163+
end
164+
165+
if tbl.text and (method == "textDocument/didChange") then
166+
local start_row = tbl.range.start.line
167+
local start_col = tbl.range.start.character
168+
local end_row = tbl.range["end"].line
169+
local end_col = tbl.range["end"].character
170+
tbl.text = table.concat(
171+
vim.api.nvim_buf_get_text(
172+
M.virtual_document_bufnr,
173+
start_row,
174+
start_col,
175+
end_row,
176+
end_col,
177+
{}
178+
),
179+
"\n"
180+
)
181+
end
182+
end
183+
184+
replace_original_uri_to_virtual_document_uri(params)
185+
186+
return params
187+
end
188+
189+
function M.rewrite_response_uris(original_uri, response)
190+
if not original_uri then
191+
return response
192+
end
193+
194+
local function replace_virtual_document_uri_with_original_uri(tbl)
195+
for key, value in pairs(tbl) do
196+
if key == M.virtual_document_uri then
197+
tbl[original_uri] = tbl[M.virtual_document_uri]
198+
tbl[M.virtual_document_uri] = nil
199+
replace_virtual_document_uri_with_original_uri(tbl[original_uri]) -- Recursive call for nested tables
200+
elseif type(value) == "table" then
201+
replace_virtual_document_uri_with_original_uri(value) -- Recursive call for nested tables
202+
elseif type(value) == "string" and value == M.virtual_document_uri then
203+
tbl[key] = original_uri
204+
end
205+
end
206+
end
207+
208+
replace_virtual_document_uri_with_original_uri(response)
209+
210+
return response
211+
end
212+
213+
function M.create_redirect_handlers()
214+
local baseHoverHandler = vim.lsp.handlers["textDocument/hover"]
215+
vim.lsp.handlers["textDocument/hover"] = function(err, res, ctx, config)
216+
if not res then
217+
return baseHoverHandler(err, res, ctx, config)
218+
end
219+
220+
local request_start_range = res.range.start
221+
local client = vim.lsp.get_client_by_id(ctx.client_id)
222+
local script_nodes = extract_script_text_nodes(0)
223+
for _, script_node in ipairs(script_nodes) do
224+
if
225+
is_position_between_range(request_start_range, script_node:range())
226+
and client.name == plugin_config.plugin_name
227+
then
228+
baseHoverHandler(err, res, ctx, config)
229+
return
230+
end
231+
end
232+
end
233+
end
234+
235+
return M

lua/typescript-tools/tsserver.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local PendingDiagnostic = require "typescript-tools.protocol.pending_diagnostic"
77
local api = require "typescript-tools.api"
88
local c = require "typescript-tools.protocol.constants"
99
local proto_utils = require "typescript-tools.protocol.utils"
10+
local html_support = require "typescript-tools.protocol.html_support"
1011

1112
---@class Tsserver
1213
---@field process Process
@@ -111,6 +112,8 @@ function Tsserver:handle_request(method, params, callback, notify_reply_callback
111112

112113
local module = module_mapper.map_method_to_module(method)
113114

115+
local is_html = vim.bo.filetype == "html"
116+
114117
-- INFO: skip sending request if it's a noop method
115118
if not module then
116119
return false
@@ -130,6 +133,8 @@ function Tsserver:handle_request(method, params, callback, notify_reply_callback
130133
method = method,
131134
}
132135

136+
local requested_buffer_uri = params and params.textDocument and params.textDocument.uri or nil
137+
133138
function handler_context.request(request)
134139
local interrupt_diagnostic = handler_module.interrupt_diagnostic
135140

@@ -153,6 +158,17 @@ function Tsserver:handle_request(method, params, callback, notify_reply_callback
153158

154159
function handler_context.response(response, error)
155160
local seq = handler_context.synthetic_seq or handler_context.seq
161+
162+
if is_html then
163+
local successfuly_rewritten, rewritten_response =
164+
pcall(html_support.rewrite_response_uris, requested_buffer_uri, vim.deepcopy(response))
165+
if successfuly_rewritten then
166+
response = rewritten_response
167+
else
168+
print([[[tsserver.lua:155] -- rewritten_response: ]] .. vim.inspect(rewritten_response))
169+
end
170+
end
171+
156172
local notify_reply = notify_reply_callback and vim.schedule_wrap(notify_reply_callback)
157173
local response_callback = callback and vim.schedule_wrap(callback)
158174

@@ -169,6 +185,16 @@ function Tsserver:handle_request(method, params, callback, notify_reply_callback
169185
end
170186
end
171187

188+
if is_html then
189+
local succesfuly_rewritten, rewritten_params =
190+
pcall(html_support.rewrite_request_uris, method, vim.deepcopy(params), requested_buffer_uri)
191+
if succesfuly_rewritten then
192+
params = rewritten_params
193+
else
194+
print([[[tsserver.lua:181] -- rewritten_params: ]] .. vim.inspect(rewritten_params))
195+
end
196+
end
197+
172198
local _, err = coroutine.resume(
173199
handler,
174200
handler_context.request,

0 commit comments

Comments
 (0)