blob: fa15a6d906ef0f7f75087037f3c7f87eaa7d8766 [file] [log] [blame]
-------------------------------------------------------------------------------
-- Copyright (c) 2011, 2012 Sierra Wireless and others.
-- All rights reserved. This program and the accompanying materials
-- are made available under the terms of the Eclipse Public License v1.0
-- which accompanies this distribution, and is available at
-- http://www.eclipse.org/legal/epl-v10.html
--
-- Contributors:
-- Sierra Wireless - initial API and implementation
-------------------------------------------------------------------------------
---
-- Uses Metalua capabilities to indent code and provide source code offset
-- semantic depth
--
-- @module luaformatter
local M = {}
require 'metalua.compiler'
local math = require 'math'
---
-- calculate all ident level
-- @param Source code to analyze
-- @return #table {linenumber = identationlevel}
-- @usage local depth = format.indentLevel("local var")
local function getindentlevel(source,indenttable)
local function getfirstline(node)
-- Regular node
local offsets = node[1].lineinfo
local first
local offset
-- Consider previous comments as part of current chunk
-- WARNING: This is NOT the default in Metalua
if offsets.first.comments then
first = offsets.first.comments.lineinfo.first.line
offset = offsets.first.comments.lineinfo.first.offset
else
first = offsets.first.line
offset = offsets.first.offset
end
return first, offset
end
local function getlastline(node)
-- Regular node
local offsets = node[#node].lineinfo
local last
-- Same for block end comments
if offsets.last.comments then
last = offsets.last.comments.lineinfo.last.line
else
last = offsets.last.line
end
return last
end
--
-- Define AST walker
--
local linetodepth = {0}
local walker = {
block = {},
expr = {},
depth = 0, -- Current depth while walking
}
function walker.block.down(node, parent,...)
--ignore empty node
if #node == 0 then
return end
-- get first line of the block
local startline,startoffset = getfirstline(node)
local endline = getlastline(node)
-- If the block don't start by a new line, don't indent the first line
if not source:sub(1,startoffset-1):find("[\r\n]%s*$") then
startline = startline + 1
end
for i=startline, endline do
linetodepth[i]=walker.depth
end
walker.depth = walker.depth + 1
end
function walker.block.up(node, ...)
if #node == 0 then
return end
walker.depth = walker.depth - 1
end
function walker.expr.down(node, parent, ...)
if indenttable and node.tag == 'Table' then
if #node == 0 then
return end
local startline,startoffset = getfirstline(node)
local endline = getlastline(node)
if source:sub(1,startoffset-1):find("[\r\n]%s*$") then
for i=startline, endline do
linetodepth[i]=walker.depth
end
else
for i=startline+1, endline do
linetodepth[i]=walker.depth
end
end
walker.depth = walker.depth + 1
elseif node.tag =='String' then
local firstline = node.lineinfo.first.line
local lastline = node.lineinfo.last.line
for i=firstline+1, lastline do
linetodepth[i]=false
end
end
end
function walker.expr.up(node, parent, ...)
if indenttable and node.tag == 'Table' then
if #node == 0 then
return end
walker.depth = walker.depth - 1
end
end
-- Walk through AST to build linetodepth
local ast = mlc.luastring_to_ast(source)
if mlc.check_ast( ast ) then
require 'metalua.walk'
walk.block(walker, ast)
end
return linetodepth
end
---
-- Trim white spaces before and after given string
--
-- @usage local trimmedstr = trim(' foo')
-- @param #string string to trim
-- @return #string string trimmed
local function trim(string)
local pattern = "^(%s*)(.*)"
local _, strip = string:match(pattern)
if not strip then return string end
local restrip
_, restrip = strip:reverse():match(pattern)
return restrip and restrip:reverse() or strip
end
---
-- Indent Lua Source Code.
-- @function [parent=#luaformatter] indentCode
-- @param source source code to format
-- @param delimiter line delimiter to use
-- @param indenttable true if you want to ident in table
-- @return #string formatted code
-- @usage indentCode('local var', '\n', true, '\t',)
-- @usage indentCode('local var', '\n', true, --[[tabulationSize]]4, --[[indentationSize]]2)
function M.indentcode(source, delimiter,indenttable, ...)
--
-- Create function which will generate indentation
--
local tabulation
if select('#', ...) > 1 then
local tabSize = select(1, ...)
local indentationSize = select(2, ...)
-- When tabulation size and indentation size is given, tabulation is
-- composed of tabulation and spaces
tabulation = function(depth)
local range = depth * indentationSize
local tabCount = math.floor(range / tabSize)
local spaceCount = range % tabSize
local tab = '\t'
local space = ' '
return tab:rep(tabCount) .. space:rep(spaceCount)
end
else
local char = select(1, ...)
-- When tabulation character is given, this character will be ducplicated
-- according to length
tabulation = function (depth) return char:rep(depth) end
end
-- Delimiter position table
-- Initialisation represent string start offset
local delimiterLength = delimiter:len()
local positions = {1-delimiterLength}
--
-- Seek for delimiters
--
local i = 1
local delimiterPosition = nil
repeat
delimiterPosition = source:find(delimiter, i, true)
if delimiterPosition then
positions[#positions + 1] = delimiterPosition
i = delimiterPosition + 1
end
until not delimiterPosition
-- No need for indentation, while no delimiters has been found
if #positions < 2 then
return source
end
-- calculate indentation
local linetodepth = getindentlevel(source,indenttable)
-- Concatenate string with right identation
local indented = {}
for position=1, #positions do
-- Extract source code line
local offset = positions[position]
-- Get the interval between two positions
local rawline
if positions[position + 1] then
rawline = source:sub(offset + delimiterLength, positions[position + 1] -1)
else
-- From current prosition to end of line
rawline = source:sub(offset + delimiterLength)
end
-- Trim white spaces
local indentcount = linetodepth[position]
if not indentcount then
indented[#indented+1] = rawline
else
local line = trim(rawline)
-- Append right indentation
-- Indent only when there is code on the line
if line:len() > 0 then
-- Compute next real depth related offset
-- As is offset is pointing a white space before first statement of block,
-- We will work with parent node depth
indented[#indented+1] = tabulation( indentcount)
-- Append timmed source code
indented[#indented+1] = line
end
end
-- Append carriage return
-- While on last character append carriage return only if at end of original source
if position < #positions or source:sub(source:len()-delimiterLength, source:len()) == delimiter then
indented[#indented+1] = delimiter
end
end
return table.concat(indented)
end
return M