Inintal commit
This commit is contained in:
+897
@@ -0,0 +1,897 @@
|
||||
#!/usr/bin/env lua
|
||||
-- SCL Language Server
|
||||
-- Language Server Protocol implementation for Siemens SCL
|
||||
|
||||
local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
|
||||
if script_path == "" then
|
||||
script_path = "."
|
||||
end
|
||||
|
||||
local cwd = io.popen("pwd"):read("*l")
|
||||
|
||||
if script_path:sub(1, 1) == "/" then
|
||||
script_path = script_path:gsub("/+$", "")
|
||||
elseif cwd then
|
||||
script_path = cwd .. "/" .. script_path
|
||||
script_path = script_path:gsub("/+", "/"):gsub("/$", "")
|
||||
else
|
||||
script_path = "."
|
||||
end
|
||||
|
||||
package.path = package.path .. ";" .. script_path .. "/?.lua"
|
||||
|
||||
local json = dofile(script_path .. "/json.lua")
|
||||
local parser = dofile(script_path .. "/parser.lua")
|
||||
local treesitter = dofile(script_path .. "/treesitter.lua")
|
||||
local plc_json = dofile(script_path .. "/plc_json.lua")
|
||||
local diagnostics = dofile(script_path .. "/diagnostics.lua")
|
||||
local formatter = dofile(script_path .. "/formatter.lua")
|
||||
|
||||
local documents = {}
|
||||
|
||||
local semanticTokenModifiers = {}
|
||||
local semanticTokenTypes = {
|
||||
"namespace",
|
||||
"type",
|
||||
"class",
|
||||
"enum",
|
||||
"interface",
|
||||
"struct",
|
||||
"typeParameter",
|
||||
"parameter",
|
||||
"variable",
|
||||
"property",
|
||||
"enumMember",
|
||||
"event",
|
||||
"func",
|
||||
"method",
|
||||
"macro",
|
||||
"keyword",
|
||||
"modifier",
|
||||
"comment",
|
||||
"string",
|
||||
"number",
|
||||
"operator",
|
||||
}
|
||||
|
||||
local capabilities = {
|
||||
hoverProvider = true,
|
||||
completionProvider = {
|
||||
triggerCharacters = { ".", "#", "(" },
|
||||
resolveProvider = false,
|
||||
},
|
||||
definitionProvider = true,
|
||||
documentSymbolProvider = true,
|
||||
diagnosticProvider = { relatedDocuments = {} },
|
||||
textDocumentSync = 1,
|
||||
semanticTokensProvider = {
|
||||
full = { delta = true },
|
||||
legend = { tokenTypes = semanticTokenTypes, tokenModifiers = semanticTokenModifiers },
|
||||
range = true,
|
||||
},
|
||||
inlayHintProvider = { resolveProvider = false },
|
||||
workspaceSymbolProvider = true,
|
||||
documentFormattingProvider = true,
|
||||
documentRangeFormattingProvider = false,
|
||||
}
|
||||
|
||||
local function uri_to_path(uri)
|
||||
return uri:gsub("^file://", ""):gsub("%%20", " ")
|
||||
end
|
||||
|
||||
local function path_to_uri(path)
|
||||
return "file://" .. path:gsub(" ", "%%20")
|
||||
end
|
||||
|
||||
local function run_command(cmd)
|
||||
local handle = io.popen(cmd)
|
||||
if not handle then
|
||||
return nil
|
||||
end
|
||||
local result = handle:read("*a")
|
||||
handle:close()
|
||||
return result
|
||||
end
|
||||
|
||||
local function scan_scl_files(root_dir)
|
||||
local files = {}
|
||||
if not root_dir or root_dir == "" then
|
||||
return files
|
||||
end
|
||||
local result = run_command("find " .. root_dir .. ' -type f -name "*.scl" 2>/dev/null')
|
||||
if result then
|
||||
for f in result:gmatch("[^\n]+") do
|
||||
if f and f ~= "" then
|
||||
table.insert(files, f)
|
||||
end
|
||||
end
|
||||
end
|
||||
return files
|
||||
end
|
||||
|
||||
local function parse_scl_file(filepath)
|
||||
local handle = io.open(filepath, "r")
|
||||
if not handle then
|
||||
return nil, nil, nil
|
||||
end
|
||||
local content = handle:read("*a")
|
||||
handle:close()
|
||||
|
||||
local variables = parser.extract_variables(content)
|
||||
local functions = parser.extract_functions(content)
|
||||
local types = parser.extract_types(content)
|
||||
|
||||
return variables, functions, types
|
||||
end
|
||||
|
||||
local workspace_symbol_cache = {}
|
||||
local function clear_symbol_cache()
|
||||
workspace_symbol_cache = {}
|
||||
end
|
||||
|
||||
local function fuzzy_match(text, pattern)
|
||||
if not text or not pattern then
|
||||
return false
|
||||
end
|
||||
text = text:lower()
|
||||
pattern = pattern:lower()
|
||||
|
||||
local text_len = #text
|
||||
local pattern_len = #pattern
|
||||
local pattern_idx = 1
|
||||
|
||||
for i = 1, text_len do
|
||||
if pattern_idx <= pattern_len and text:sub(i, i) == pattern:sub(pattern_idx, pattern_idx) then
|
||||
pattern_idx = pattern_idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
return pattern_idx > pattern_len
|
||||
end
|
||||
|
||||
local handlers = {}
|
||||
|
||||
function handlers.initialize(params)
|
||||
return {
|
||||
capabilities = capabilities,
|
||||
serverInfo = { name = "scl-language-server", version = "1.0.0" },
|
||||
}
|
||||
end
|
||||
|
||||
function handlers.initialized(params) end
|
||||
|
||||
function handlers.shutdown(params)
|
||||
return nil
|
||||
end
|
||||
|
||||
function handlers.exit(params)
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
function handlers.textDocument_didOpen(params)
|
||||
local uri = params.textDocument.uri
|
||||
local content = params.textDocument.text
|
||||
local path = uri_to_path(uri)
|
||||
local root_dir = path:match("(.*/)") or "."
|
||||
|
||||
local variables, var_positions = parser.extract_variables(content)
|
||||
local functions = parser.extract_functions(content)
|
||||
local types = parser.extract_types(content)
|
||||
|
||||
local external_types = plc_json.get_types(root_dir)
|
||||
for name, ext_type in pairs(external_types) do
|
||||
if not types[name] then
|
||||
types[name] = ext_type
|
||||
end
|
||||
end
|
||||
|
||||
local tree = nil
|
||||
local ts_ok = treesitter.init()
|
||||
if ts_ok then
|
||||
tree, _ = treesitter.parse_scl(content)
|
||||
end
|
||||
|
||||
documents[uri] = {
|
||||
content = content,
|
||||
version = params.textDocument.version,
|
||||
path = path,
|
||||
root_dir = root_dir,
|
||||
variables = variables,
|
||||
variable_positions = var_positions,
|
||||
functions = functions,
|
||||
types = types,
|
||||
tree = tree,
|
||||
}
|
||||
end
|
||||
|
||||
function handlers.textDocument_didChange(params)
|
||||
local uri = params.textDocument.uri
|
||||
local content = params.contentChanges[1].text
|
||||
|
||||
local variables, var_positions = parser.extract_variables(content)
|
||||
local functions = parser.extract_functions(content)
|
||||
local types = parser.extract_types(content)
|
||||
|
||||
local doc = documents[uri]
|
||||
local root_dir = doc and doc.root_dir or ""
|
||||
local external_types = plc_json.get_types(root_dir)
|
||||
for name, ext_type in pairs(external_types) do
|
||||
if not types[name] then
|
||||
types[name] = ext_type
|
||||
end
|
||||
end
|
||||
|
||||
local tree = nil
|
||||
local ts_ok = treesitter.init()
|
||||
if ts_ok then
|
||||
tree, _ = treesitter.parse_scl(content)
|
||||
end
|
||||
|
||||
documents[uri] = {
|
||||
content = content,
|
||||
version = params.textDocument.version,
|
||||
path = doc and doc.path or "",
|
||||
root_dir = root_dir,
|
||||
variables = variables,
|
||||
variable_positions = var_positions,
|
||||
functions = functions,
|
||||
types = types,
|
||||
tree = tree,
|
||||
}
|
||||
|
||||
clear_symbol_cache()
|
||||
end
|
||||
|
||||
function handlers.textDocument_didClose(params)
|
||||
documents[params.textDocument.uri] = nil
|
||||
clear_symbol_cache()
|
||||
end
|
||||
|
||||
function handlers.textDocument_hover(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return nil
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in doc.content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
if #lines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local line_idx = params.position.line + 1
|
||||
if line_idx > #lines then
|
||||
return nil
|
||||
end
|
||||
|
||||
local line = lines[line_idx]
|
||||
local col = params.position.character + 1
|
||||
|
||||
local word_start = col
|
||||
while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do
|
||||
word_start = word_start - 1
|
||||
end
|
||||
|
||||
local word_end = col
|
||||
while word_end <= #line and line:sub(word_end, word_end):match("[%w_]") do
|
||||
word_end = word_end + 1
|
||||
end
|
||||
|
||||
local word = line:sub(word_start, word_end - 1)
|
||||
local var_name = word:gsub("^#", "")
|
||||
|
||||
if var_name ~= "" and doc.variables and doc.variables[var_name] then
|
||||
local var_info = doc.variables[var_name]
|
||||
local detail = string.format(
|
||||
"**Local Variable** `#%s`\nScope: `%s`\nType: `%s`\nDeclared at line %d",
|
||||
var_name,
|
||||
var_info.type,
|
||||
var_info.data_type or "unknown",
|
||||
var_info.line + 1
|
||||
)
|
||||
return {
|
||||
contents = { kind = "markdown", value = detail },
|
||||
range = {
|
||||
start = { line = params.position.line, character = word_start - 1 },
|
||||
endPos = { line = params.position.line, character = word_end - 1 },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
if doc.types then
|
||||
for type_name, type_info in pairs(doc.types) do
|
||||
if type_name == word then
|
||||
local detail = string.format("**Type**: `%s`\nKind: %s", type_name, type_info.kind)
|
||||
if type_info.kind == "struct" and type_info.fields then
|
||||
detail = detail .. "\n\n**Fields:**\n"
|
||||
for _, field in ipairs(type_info.fields) do
|
||||
detail = detail .. string.format("- `%s`: %s\n", field.name, field.type)
|
||||
end
|
||||
end
|
||||
return {
|
||||
contents = { kind = "markdown", value = detail },
|
||||
range = {
|
||||
start = { line = params.position.line, character = word_start - 1 },
|
||||
endPos = { line = params.position.line, character = word_end - 1 },
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function handlers.textDocument_completion(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return { items = {} }
|
||||
end
|
||||
|
||||
local items = {}
|
||||
local lines = {}
|
||||
for line in doc.content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
local line_idx = params.position.line + 1
|
||||
if line_idx <= #lines then
|
||||
local line = lines[line_idx]
|
||||
local col = params.position.character
|
||||
local trigger = col > 0 and line:sub(col, col) or ""
|
||||
|
||||
if trigger == "#" then
|
||||
if doc.variables then
|
||||
for name, info in pairs(doc.variables) do
|
||||
table.insert(items, {
|
||||
label = "#" .. name,
|
||||
kind = 13,
|
||||
detail = "Local variable (" .. (info.type or "unknown") .. ")",
|
||||
insertText = "#" .. name,
|
||||
insertTextFormat = 1,
|
||||
})
|
||||
end
|
||||
end
|
||||
elseif trigger == "." then
|
||||
local prefix_start = col - 1
|
||||
while prefix_start > 0 and line:sub(prefix_start, prefix_start):match("[%w_]") do
|
||||
prefix_start = prefix_start - 1
|
||||
end
|
||||
local prefix = line:sub(prefix_start + 1, col - 1)
|
||||
|
||||
if prefix ~= "" then
|
||||
local var_name = prefix:gsub("^#", "")
|
||||
local var_info = doc.variables and doc.variables[var_name]
|
||||
local data_type = var_info and var_info.data_type
|
||||
|
||||
if data_type and doc.types then
|
||||
local struct_name = data_type
|
||||
if doc.types[struct_name] and doc.types[struct_name].fields then
|
||||
for _, field in ipairs(doc.types[struct_name].fields) do
|
||||
table.insert(items, {
|
||||
label = field.name,
|
||||
kind = 13,
|
||||
detail = struct_name .. "." .. field.name .. ": " .. field.type,
|
||||
insertText = field.name,
|
||||
insertTextFormat = 1,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #items == 0 then
|
||||
local keywords = {
|
||||
{ label = "AND", detail = "Logical AND", kind = 14 },
|
||||
{ label = "OR", detail = "Logical OR", kind = 14 },
|
||||
{ label = "XOR", detail = "Logical XOR", kind = 14 },
|
||||
{ label = "NOT", detail = "Logical NOT", kind = 14 },
|
||||
{ label = "MOD", detail = "Modulo", kind = 14 },
|
||||
}
|
||||
for _, kw in ipairs(keywords) do
|
||||
table.insert(items, kw)
|
||||
end
|
||||
end
|
||||
elseif trigger == "(" then
|
||||
if doc.functions then
|
||||
for _, func in ipairs(doc.functions) do
|
||||
table.insert(items, {
|
||||
label = func.name,
|
||||
kind = 3,
|
||||
detail = func.kind:upper(),
|
||||
insertText = func.name .. "()",
|
||||
insertTextFormat = 2,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return { isIncomplete = false, items = items }
|
||||
end
|
||||
|
||||
function handlers.textDocument_definition(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return nil
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in doc.content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
if params.position.line + 1 > #lines then
|
||||
return nil
|
||||
end
|
||||
|
||||
local line = lines[params.position.line + 1]
|
||||
local col = params.position.character + 1
|
||||
|
||||
local word_start = col
|
||||
while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do
|
||||
word_start = word_start - 1
|
||||
end
|
||||
|
||||
local word_end = col
|
||||
while word_end <= #line and line:sub(word_end, word_end):match("[%w_]") do
|
||||
word_end = word_end + 1
|
||||
end
|
||||
|
||||
local word = line:sub(word_start, word_end - 1):gsub("^#", "")
|
||||
if doc.variable_positions and doc.variable_positions[word] then
|
||||
local pos = doc.variable_positions[word]
|
||||
return {
|
||||
uri = uri,
|
||||
range = {
|
||||
start = { line = pos.line, character = pos.character },
|
||||
endPos = { line = pos.line, character = pos.character + #word },
|
||||
},
|
||||
}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function handlers.textDocument_documentSymbol(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return {}
|
||||
end
|
||||
|
||||
local symbols = {}
|
||||
local kind_map = {
|
||||
["function"] = 14,
|
||||
["function_block"] = 11,
|
||||
["organization_block"] = 25,
|
||||
["struct"] = 23,
|
||||
["alias"] = 7,
|
||||
["array"] = 24,
|
||||
}
|
||||
|
||||
if doc.functions then
|
||||
for _, func in ipairs(doc.functions) do
|
||||
table.insert(symbols, {
|
||||
name = func.kind:upper() .. " " .. func.name,
|
||||
kind = kind_map[func.kind] or 14,
|
||||
range = { start = { line = func.start, character = 0 }, endPos = { line = func.final, character = 0 } },
|
||||
selectionRange = {
|
||||
start = { line = func.start, character = 0 },
|
||||
endPos = { line = func.start, character = #func.name },
|
||||
},
|
||||
containerName = nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if doc.variables then
|
||||
for var_name, var_info in pairs(doc.variables) do
|
||||
local var_kind = 13
|
||||
if var_info.type and (var_info.type:find("CONSTANT") or var_info.type:find("RETAIN")) then
|
||||
var_kind = 26
|
||||
end
|
||||
table.insert(symbols, {
|
||||
name = "#" .. var_name,
|
||||
kind = var_kind,
|
||||
detail = var_info.type or "VAR",
|
||||
range = {
|
||||
start = { line = var_info.line, character = 0 },
|
||||
endPos = { line = var_info.line, character = #var_name },
|
||||
},
|
||||
selectionRange = {
|
||||
start = { line = var_info.line, character = var_info.character },
|
||||
endPos = { line = var_info.line, character = var_info.character + #var_name },
|
||||
},
|
||||
containerName = nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if doc.types then
|
||||
for type_name, type_info in pairs(doc.types) do
|
||||
local kind = kind_map[type_info.kind] or 7
|
||||
local detail = type_info.kind:upper()
|
||||
if type_info.kind == "alias" and type_info.base_type then
|
||||
detail = detail .. " (" .. type_info.base_type .. ")"
|
||||
elseif type_info.kind == "struct" and type_info.fields and #type_info.fields > 0 then
|
||||
detail = detail .. " (" .. #type_info.fields .. " fields)"
|
||||
end
|
||||
table.insert(symbols, {
|
||||
name = "TYPE " .. type_name,
|
||||
kind = kind,
|
||||
detail = detail,
|
||||
range = {
|
||||
start = { line = type_info.start_line or 0, character = 0 },
|
||||
endPos = { line = type_info.start_line or 0, character = #type_name },
|
||||
},
|
||||
selectionRange = {
|
||||
start = { line = type_info.start_line or 0, character = 0 },
|
||||
endPos = { line = type_info.start_line or 0, character = #type_name },
|
||||
},
|
||||
containerName = nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return symbols
|
||||
end
|
||||
|
||||
function handlers.textDocument_diagnostic(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return { items = {} }
|
||||
end
|
||||
|
||||
local external_types = {}
|
||||
if doc.root_dir then
|
||||
external_types = plc_json.get_types(doc.root_dir)
|
||||
end
|
||||
|
||||
local diag_results = diagnostics.validate_diagnostics(doc.content, external_types, doc.root_dir)
|
||||
return { items = diag_results }
|
||||
end
|
||||
|
||||
function handlers.textDocument_semanticTokensFull(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return { data = {} }
|
||||
end
|
||||
|
||||
local tokens = {}
|
||||
local lines = {}
|
||||
for line in doc.content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
local function add_token(line, startChar, length, tokenType)
|
||||
local deltaLine = line
|
||||
local deltaStart = startChar
|
||||
if #tokens >= 4 then
|
||||
local prevLine = tokens[#tokens - 3]
|
||||
local prevStart = tokens[#tokens - 2]
|
||||
if line == prevLine then
|
||||
deltaLine = 0
|
||||
deltaStart = startChar - prevStart
|
||||
else
|
||||
deltaLine = line - prevLine
|
||||
end
|
||||
end
|
||||
table.insert(tokens, deltaLine)
|
||||
table.insert(tokens, deltaStart)
|
||||
table.insert(tokens, length)
|
||||
table.insert(tokens, 0)
|
||||
table.insert(tokens, 0)
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
local lineNum = i - 1
|
||||
local trimmed = line:gsub("^%s+", "")
|
||||
|
||||
if doc.variables then
|
||||
for var_name, info in pairs(doc.variables) do
|
||||
if info.line == lineNum then
|
||||
local startChar = line:find(var_name, 1, true)
|
||||
if startChar then
|
||||
add_token(lineNum, startChar - 1, #var_name, "variable")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local keywords = {
|
||||
"IF",
|
||||
"THEN",
|
||||
"ELSIF",
|
||||
"ELSE",
|
||||
"END_IF",
|
||||
"FOR",
|
||||
"TO",
|
||||
"BY",
|
||||
"DO",
|
||||
"END_FOR",
|
||||
"WHILE",
|
||||
"END_WHILE",
|
||||
"REPEAT",
|
||||
"UNTIL",
|
||||
"END_REPEAT",
|
||||
"CASE",
|
||||
"OF",
|
||||
"END_CASE",
|
||||
"VAR_INPUT",
|
||||
"VAR_OUTPUT",
|
||||
"VAR_IN_OUT",
|
||||
"VAR_TEMP",
|
||||
"VAR",
|
||||
"END_VAR",
|
||||
"BEGIN",
|
||||
"EXIT",
|
||||
"CONTINUE",
|
||||
"RETURN",
|
||||
"TRUE",
|
||||
"FALSE",
|
||||
}
|
||||
for _, kw in ipairs(keywords) do
|
||||
local startPos = line:find("%f[%a]" .. kw .. "%f[%A]")
|
||||
if startPos then
|
||||
add_token(lineNum, startPos - 1, #kw, "keyword")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return { data = tokens }
|
||||
end
|
||||
|
||||
function handlers.textDocument_semanticTokensRange(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return { data = {} }
|
||||
end
|
||||
return handlers.textDocument_semanticTokensFull(params)
|
||||
end
|
||||
|
||||
function handlers.textDocument_inlayHint(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return {}
|
||||
end
|
||||
|
||||
local hints = {}
|
||||
local lines = {}
|
||||
for line in doc.content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
local lineNum = i - 1
|
||||
if doc.variables then
|
||||
for var_name, info in pairs(doc.variables) do
|
||||
if info.line == lineNum and info.data_type and info.data_type ~= "unknown" then
|
||||
local colonPos = line:find(":", 1, true)
|
||||
if colonPos then
|
||||
table.insert(hints, {
|
||||
position = { line = lineNum, character = #line },
|
||||
label = ": " .. info.data_type,
|
||||
kind = 1,
|
||||
paddingLeft = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return hints
|
||||
end
|
||||
|
||||
function handlers.workspace_symbol(params)
|
||||
local query = params.query or ""
|
||||
local max_results = params.maxResults or 50
|
||||
|
||||
if query == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local results = {}
|
||||
|
||||
local function add_result(name, kind, uri, line, col, length)
|
||||
table.insert(results, {
|
||||
name = name,
|
||||
kind = kind,
|
||||
location = {
|
||||
uri = uri,
|
||||
range = {
|
||||
start = { line = line, character = col },
|
||||
endPos = { line = line, character = col + length },
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
for uri, doc in pairs(documents) do
|
||||
if doc.functions then
|
||||
for _, func in ipairs(doc.functions) do
|
||||
if fuzzy_match(func.name, query) or fuzzy_match(func.kind, query) then
|
||||
add_result(
|
||||
func.kind:upper() .. " " .. func.name,
|
||||
func.kind == "function" and 14 or (func.kind == "function_block" and 11 or 25),
|
||||
uri,
|
||||
func.start,
|
||||
0,
|
||||
#func.name
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if doc.variables then
|
||||
for var_name, var_info in pairs(doc.variables) do
|
||||
if fuzzy_match(var_name, query) then
|
||||
add_result("#" .. var_name, 13, uri, var_info.line, var_info.character, #var_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if doc.types then
|
||||
for type_name, type_info in pairs(doc.types) do
|
||||
if fuzzy_match(type_name, query) then
|
||||
local kind = 7
|
||||
if type_info.kind == "struct" then
|
||||
kind = 23
|
||||
elseif type_info.kind == "array" then
|
||||
kind = 24
|
||||
end
|
||||
add_result("TYPE " .. type_name, kind, uri, type_info.start_line or 0, 0, #type_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local scanned_dirs = {}
|
||||
for _, doc in pairs(documents) do
|
||||
if doc.root_dir and doc.root_dir ~= "" then
|
||||
scanned_dirs[doc.root_dir] = true
|
||||
end
|
||||
end
|
||||
|
||||
local function search_file(filepath)
|
||||
local cached_file = filepath
|
||||
if not workspace_symbol_cache[":file:" .. cached_file] then
|
||||
local variables, functions, types = parse_scl_file(filepath)
|
||||
workspace_symbol_cache[":file:" .. cached_file] = {
|
||||
variables = variables,
|
||||
functions = functions,
|
||||
types = types,
|
||||
}
|
||||
end
|
||||
|
||||
local cached_data = workspace_symbol_cache[":file:" .. cached_file]
|
||||
local file_uri = path_to_uri(filepath)
|
||||
|
||||
if cached_data.functions then
|
||||
for _, func in ipairs(cached_data.functions) do
|
||||
if fuzzy_match(func.name, query) or fuzzy_match(func.kind, query) then
|
||||
add_result(
|
||||
func.kind:upper() .. " " .. func.name,
|
||||
func.kind == "function" and 14 or (func.kind == "function_block" and 11 or 25),
|
||||
file_uri,
|
||||
func.start,
|
||||
0,
|
||||
#func.name
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if cached_data.variables then
|
||||
for var_name, var_info in pairs(cached_data.variables) do
|
||||
if fuzzy_match(var_name, query) then
|
||||
add_result("#" .. var_name, 13, file_uri, var_info.line, var_info.character, #var_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if cached_data.types then
|
||||
for type_name, type_info in pairs(cached_data.types) do
|
||||
if fuzzy_match(type_name, query) then
|
||||
local kind = 7
|
||||
if type_info.kind == "struct" then
|
||||
kind = 23
|
||||
elseif type_info.kind == "array" then
|
||||
kind = 24
|
||||
end
|
||||
add_result("TYPE " .. type_name, kind, file_uri, type_info.start_line or 0, 0, #type_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for root_dir, _ in pairs(scanned_dirs) do
|
||||
local scl_files = scan_scl_files(root_dir)
|
||||
for _, filepath in ipairs(scl_files) do
|
||||
search_file(filepath)
|
||||
end
|
||||
end
|
||||
|
||||
local final_results = {}
|
||||
for i = 1, math.min(#results, max_results) do
|
||||
table.insert(final_results, results[i])
|
||||
end
|
||||
|
||||
return final_results
|
||||
end
|
||||
|
||||
function handlers.textDocument_formatting(params)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
if not doc then
|
||||
return nil
|
||||
end
|
||||
|
||||
local options = params.options or {}
|
||||
local format_options = {
|
||||
insertSpaces = options.insertSpaces,
|
||||
tabSize = options.tabSize,
|
||||
indentSize = options.tabSize or 4,
|
||||
}
|
||||
|
||||
return formatter.format_document(doc.content, format_options)
|
||||
end
|
||||
|
||||
local function handle_message(message)
|
||||
if message.method == "$/cancelRequest" then
|
||||
return nil
|
||||
end
|
||||
local handler = handlers[message.method]
|
||||
if handler then
|
||||
local ok, result = pcall(handler, message.params)
|
||||
if ok then
|
||||
return { id = message.id, result = result }
|
||||
else
|
||||
return { id = message.id, error = { code = -32603, message = tostring(result) } }
|
||||
end
|
||||
else
|
||||
return { id = message.id, error = { code = -32601, message = "Method not found: " .. message.method } }
|
||||
end
|
||||
end
|
||||
|
||||
local function main()
|
||||
local stdin = io.input()
|
||||
local stdout = io.output()
|
||||
local content_length = nil
|
||||
|
||||
while true do
|
||||
local line = stdin:read("*l")
|
||||
if not line then
|
||||
break
|
||||
end
|
||||
if line:match("^Content%-Length:%s*(%d+)") then
|
||||
content_length = tonumber(line:match("^Content%-Length:%s*(%d+)"))
|
||||
elseif line == "" then
|
||||
if content_length then
|
||||
local body = stdin:read(content_length)
|
||||
local message = json.decode(body)
|
||||
if message.method == "exit" then
|
||||
break
|
||||
end
|
||||
local response = handle_message(message)
|
||||
if response then
|
||||
local response_body = json.encode(response)
|
||||
stdout:write("Content-Length: " .. #response_body .. "\r\n")
|
||||
stdout:write("Content-Type: application/json\r\n\r\n")
|
||||
stdout:write(response_body)
|
||||
stdout:flush()
|
||||
end
|
||||
content_length = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user