diff --git a/src/index.ts b/src/index.ts index ec7ff05..7ba3569 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,10 +7,11 @@ import { Go } from './languages/go'; import { HTML } from './languages/html'; import { Java } from './languages/java'; import { Javascript } from './languages/javascript'; +import { Julia } from './languages/julia'; +import { Lua } from './languages/lua'; import { PHP } from './languages/php'; import { Python } from './languages/python'; import { Ruby } from './languages/ruby'; -import { Julia } from './languages/julia'; import { Rust } from './languages/rust'; import { SQL } from './languages/sql'; import { nearTop, getPoints } from './points'; @@ -46,6 +47,7 @@ const languages: Record = { Java, Javascript, Julia, + Lua, PHP, Python, Ruby, diff --git a/src/languages/c.ts b/src/languages/c.ts index eca57ac..7f06120 100644 --- a/src/languages/c.ts +++ b/src/languages/c.ts @@ -46,4 +46,6 @@ export const C: LanguagePattern[] = [ /(new|this\s)?(List|IEnumerable)<(sbyte|byte|short|ushort|int|uint|long|ulong|float|double|decimal|bool|char|string)>/, type: 'not', }, + // Avoiding Lua confusion + { pattern: /local\s(function|\w+)?/, type: 'not' }, ]; diff --git a/src/languages/javascript.ts b/src/languages/javascript.ts index fd4f842..2c74b78 100644 --- a/src/languages/javascript.ts +++ b/src/languages/javascript.ts @@ -35,4 +35,6 @@ export const Javascript: LanguagePattern[] = [ { pattern: /(using\s)?System(\..*)?(;)?/, type: 'not' }, { pattern: /(func|fn)\s/, type: 'not' }, { pattern: /(begin|end)\n/, type: 'not' }, + // Avoiding Lua confusion + { pattern: /local\s(function|(\w+)\s=)/, type: 'not' }, ]; diff --git a/src/languages/julia.ts b/src/languages/julia.ts index e737ee5..6ac955c 100644 --- a/src/languages/julia.ts +++ b/src/languages/julia.ts @@ -29,4 +29,6 @@ export const Julia: LanguagePattern[] = [ { pattern: /def\s+\w+\s*(\(.+\))?\s*\n/, type: 'not' }, { pattern: /puts\s+("|').+("|')/, type: 'not' }, { pattern: /class\s/, type: 'not' }, + // Avoiding Lua confusion + { pattern: /local\s(function|\w+)/, type: 'not' }, ]; diff --git a/src/languages/lua.ts b/src/languages/lua.ts new file mode 100644 index 0000000..a78a06f --- /dev/null +++ b/src/languages/lua.ts @@ -0,0 +1,63 @@ +import type { LanguagePattern } from '../types'; + +export const Lua: LanguagePattern[] = [ + // multiline string + { pattern: /(\[\[.*\]\])/, type: 'constant.string' }, + // local definition + { pattern: /local\s([a-zA-Z0-9_]+)(\s*=)?/, type: 'keyword.variable' }, + // function definition + { pattern: /(local\s)?function\s*([a-zA-Z0-9_]*)?\(\)/, type: 'keyword.function' }, + // for loop + { pattern: /for\s+([a-zA-Z]+)\s*=\s*([a-zA-Z0-9_]+),\s*([a-zA-Z0-9_]+)\s+do/, type: 'keyword.control' }, + // while loop + { pattern: /while\s(.*)\sdo/, type: 'keyword.control' }, + // keywords + { + pattern: + /\s+(and|break|do|else|elseif|end|false|function|if|in|not|or|local|repeat|return|then|true|until|pairs|ipairs|in|yield)/, + type: 'keyword.other', + }, + { pattern: /nil/, type: 'constant.null' }, + // length operator + { pattern: /#([a-zA-Z_{}]+)/, type: 'keyword.operator' }, + // metatables + { pattern: /((get|set)metatable|raw(get|set|equal))\(.*\)/, type: 'keyword.other' }, + // metamethods + { pattern: /__(index|newindex|call|sub|mul|div|mod|pow|unm|eq|le|lt)/, type: 'keyword.other' }, + // method invocation + { pattern: /(\(.+\)|([a-zA-Z_]+)):([a-zA-Z_])\(.*\)/, type: 'keyword.other' }, + // array-like table + { pattern: /{\s*(\S+)((,|;)\s*\S+)*\s*}/, type: 'constant.array' }, + // map-like table + { pattern: /{\s*(\S+\s*=\s*\S+)((,|;)\s*\S+\s*=\s*\S+)*\s*}/, type: 'constant.dictionary' }, + // builtin math methods + { pattern: /math\.(.*)\([0-9]*\)/, type: 'macro' }, + // builtin table methods + { pattern: /table\.(.*)\(.*\)/, type: 'macro' }, + // builtin io methods + { pattern: /io\.(.*)\(.*\)/, type: 'macro' }, + // builtin functions + { pattern: /(require|dofile)\((.*)\)/, type: 'meta.import' }, + { pattern: /(pcall|xpcall|unpack|pack|coroutine)/, type: 'keyword.other' }, + // comments + { pattern: /--(\[\[)?.*/, type: 'comment.line' }, + // rest arguments + { pattern: /\.\.\./, type: 'keyword.other' }, + + // invalid comments + { pattern: /(\/\/|\/\*)/, type: 'not' }, + // avoid confusion with C + { pattern: /(#(include|define)|printf|\s+int\s+)/, type: 'not' }, + // avoid confusion with javascript + { pattern: /\s+(let|const|var)\s+/, type: 'not' }, + // avoid confusion with PHP & Python + { pattern: /\s+(echo|die|\$(.*))\s+/, type: 'not' }, + // avoid confusion with Python + { pattern: /(def|len|from|import)/, type: 'not' }, + // avoid confusion with SQL + { pattern: /(SELECT|FROM|INSERT|ALTER)/, type: 'not' }, + // avoid confusion with Ruby + { pattern: /(puts)/, type: 'not' }, + // avoid confusion Julia + { pattern: /(([a-zA-Z0-9]+)::([a-zA-Z0-9]+)|using|(.*)!\(.*\)|(\|\|))/, type: 'not' }, +]; diff --git a/src/languages/php.ts b/src/languages/php.ts index 69b9a6e..5cce687 100644 --- a/src/languages/php.ts +++ b/src/languages/php.ts @@ -31,4 +31,6 @@ export const PHP: LanguagePattern[] = [ { pattern: /(^|\s)(var|char|long|int|float|double)\s+\w+\s*=?/, type: 'not' }, // Javascript variable declaration { pattern: /(var|const|let)\s+\w+\s*=?/, type: 'not' }, + // Avoiding Lua confusion + { pattern: /local\s(function|\w+)/, type: 'not' }, ]; diff --git a/src/languages/python.ts b/src/languages/python.ts index 8e72142..e9c673b 100644 --- a/src/languages/python.ts +++ b/src/languages/python.ts @@ -25,4 +25,7 @@ export const Python: LanguagePattern[] = [ { pattern: /print((\s*\(.+\))|\s+.+)/, type: 'keyword.print' }, // &&/|| operators { pattern: /(&{2}|\|{2})/, type: 'not' }, + // avoiding lua + { pattern: /elseif/, type: 'not' }, + { pattern: /local\s(function|\w+)?\s=\s/, type: 'not' }, ]; diff --git a/src/languages/sql.ts b/src/languages/sql.ts index 2809d0c..b7bb55b 100644 --- a/src/languages/sql.ts +++ b/src/languages/sql.ts @@ -19,4 +19,7 @@ export const SQL: LanguagePattern[] = [ { pattern: /(BIT|TINYINT|SMALLINT|MEDIUMINT|INT|INTEGER|BIGINT|DOUBLE)\([0-9]+\)/, type: 'constant.type' }, { pattern: /(TINYBLOB|TINYTEXT|MEDIUMTEXT|MEDIUMBLOB|LONGTEXT|LONGBLOB)/, type: 'constant.type' }, { pattern: /(BOOLEAN|BOOL|DATE|YEAR)/, type: 'constant.type' }, + // Avoiding Lua + { pattern: /local\s(function|\w+)?\s=\s/, type: 'not' }, + { pattern: /(require|dofile)\((.*)\)/, type: 'not' }, ]; diff --git a/tests/cpp.test.ts b/tests/cpp.test.ts index b7d3972..a870dbc 100644 --- a/tests/cpp.test.ts +++ b/tests/cpp.test.ts @@ -14,18 +14,19 @@ test('hello world', () => { C: 0, 'C++': 5, CSS: 0, + 'C#': 0, Go: 0, HTML: 0, Java: 0, Javascript: 0, Julia: 2, + Lua: 2, PHP: 0, Python: 0, Ruby: 0, Rust: 0, SQL: 0, Unknown: 1, - 'C#': 0, }); }); diff --git a/tests/cs.test.ts b/tests/cs.test.ts index 1bd5866..06d3af4 100644 --- a/tests/cs.test.ts +++ b/tests/cs.test.ts @@ -20,6 +20,7 @@ test('hello world', () => { Java: -40, Javascript: -40, Julia: 5, + Lua: -20, PHP: 0, Python: 0, Ruby: 0, diff --git a/tests/large.test.ts b/tests/large.test.ts index 9b85672..2b23ab0 100644 --- a/tests/large.test.ts +++ b/tests/large.test.ts @@ -667,6 +667,7 @@ test('large input', () => { Java: -832, Javascript: -452, Julia: 24, + Lua: -1820, PHP: -318, Python: -140, Ruby: 0, diff --git a/tests/lua.test.ts b/tests/lua.test.ts new file mode 100644 index 0000000..f78b72d --- /dev/null +++ b/tests/lua.test.ts @@ -0,0 +1,304 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import detectLang from '../src/index'; + +test('local definition', () => { + const code = detectLang( + `local foo = "bar" + local some_var = 12 + local table = {1, 2, "foo"} `, + ); + assert.equal(code, 'Lua'); +}); + +test('array-like tables', () => { + const code = detectLang(`{1212, "foo", 'bar', true, false, nil}`); + + assert.equal(code, 'Lua'); +}); + +test('map-like tables', () => { + const code = detectLang(`{foo = "bar", [0] = false, ["true"] = 1212}`); + + assert.equal(code, 'Lua'); +}); + +test('metatable definition', () => { + const code = detectLang(`local foo = setmetatable({}, { + __index = function(x, y) return y end + __newindex = function(x, y) rawset(x, y) end + __add = function(x, y) return x + y end + })`); + + assert.equal(code, 'Lua'); +}); + +test('functiopn call', () => { + const code = detectLang(` + foo("bar") + foo "bar" + foo_fasda 'bar' + foo [[bar]] + foo"bar" + foo'bar' + foo[[bar]] + foo {...} + foo{...} + `); + + assert.equal(code, 'Lua'); +}); + +test('http', () => { + const code = detectLang( + `for i = 1, 100 do + local http = require("socket.http") + local url = require("socket.url") + local page = http.request('http://www.google.com/m/search?q=' .. url.escape("lua")) + print(page)`, + ); + + assert.equal(code, 'Lua'); +}); + +test('fizzbuzz', () => { + const code = detectLang( + `for i = 1, 100 do + if i % 15 == 0 then + print("FizzBuzz") + elseif i % 3 == 0 then + print("Fizz") + elseif i % 5 == 0 then + print("Buzz") + else + print(i) + end + end`, + ); + + assert.equal(code, 'Lua'); +}); + +test('fibonacci sequence', () => { + const code = detectLang(`--calculates the nth fibonacci number. Breaks for negative or non-integer n. + function fibs(n) + return n < 2 and n or fibs(n - 1) + fibs(n - 2) + end`); + assert.equal(code, 'Lua'); +}); + +test('quicksort', () => { + const code = detectLang(`--in-place quicksort + function quicksort(t, start, endi) + start, endi = start or 1, endi or #t + --partition w.r.t. first element + if(endi - start < 1) then return t end + local pivot = start + for i = start + 1, endi do + if t[i] <= t[pivot] then + if i == pivot + 1 then + t[pivot],t[pivot+1] = t[pivot+1],t[pivot] + else + t[pivot],t[pivot+1],t[i] = t[i],t[pivot],t[pivot+1] + end + pivot = pivot + 1 + end + end + t = quicksort(t, start, pivot - 1) + return quicksort(t, pivot + 1, endi) + end + + --example + print(unpack(quicksort{5, 2, 7, 3, 4, 7, 1}))`); + + assert.equal(code, 'Lua'); +}); + +test('floyd warshall', () => { + const code = detectLang(`function printResult(dist, nxt) + print("pair dist path") + for i=0, #nxt do + for j=0, #nxt do + if i ~= j then + u = i + 1 + v = j + 1 + path = string.format("%d -> %d %2d %s", u, v, dist[i][j], u) + repeat + u = nxt[u-1][v-1] + path = path .. " -> " .. u + until (u == v) + print(path) + end + end + end + end + + function floydWarshall(weights, numVertices) + dist = {} + for i=0, numVertices-1 do + dist[i] = {} + for j=0, numVertices-1 do + dist[i][j] = math.huge + end + end + + for _,w in pairs(weights) do + -- the weights array is one based + dist[w[1]-1][w[2]-1] = w[3] + end + + nxt = {} + for i=0, numVertices-1 do + nxt[i] = {} + for j=0, numVertices-1 do + if i ~= j then + nxt[i][j] = j+1 + end + end + end + + for k=0, numVertices-1 do + for i=0, numVertices-1 do + for j=0, numVertices-1 do + if dist[i][k] + dist[k][j] < dist[i][j] then + dist[i][j] = dist[i][k] + dist[k][j] + nxt[i][j] = nxt[i][k] + end + end + end + end + + printResult(dist, nxt) + end + + weights = { + {1, 3, -2}, + {2, 1, 4}, + {2, 3, 3}, + {3, 4, 2}, + {4, 2, -1} + } + numVertices = 4 + floydWarshall(weights, numVertices)`); + + assert.equal(code, 'Lua'); +}); + +test('bubble sort', () => { + const code = detectLang(`function bubbleSort(A) + local itemCount=#A + local hasChanged + repeat + hasChanged = false + itemCount=itemCount - 1 + for i = 1, itemCount do + if A[i] > A[i + 1] then + A[i], A[i + 1] = A[i + 1], A[i] + hasChanged = true + end + end + until hasChanged == false + end + + list = { 5, 6, 1, 2, 9, 14, 2, 15, 6, 7, 8, 97 } + bubbleSort(list) + for i, j in pairs(list) do + print(j) + end`); + + assert.equal(code, 'Lua'); +}); + +test('ludic numbers', () => { + const code = detectLang(`-- Return table of ludic numbers below limit + function ludics (limit) + local ludList, numList, index = {1}, {} + for n = 2, limit do table.insert(numList, n) end + while #numList > 0 do + index = numList[1] + table.insert(ludList, index) + for key = #numList, 1, -1 do + if key % index == 1 then table.remove(numList, key) end + end + end + return ludList + end + + -- Return true if n is found in t or false otherwise + function foundIn (t, n) + for k, v in pairs(t) do + if v == n then return true end + end + return false + end + + -- Display msg followed by all values in t + function show (msg, t) + io.write(msg) + for _, v in pairs(t) do io.write(" " .. v) end + print("\n") + end + + -- Main procedure + local first25, under1k, inRange, tripList, triplets = {}, 0, {}, {}, {} + for k, v in pairs(ludics(30000)) do + if k <= 25 then table.insert(first25, v) end + if v <= 1000 then under1k = under1k + 1 end + if k >= 2000 and k <= 2005 then table.insert(inRange, v) end + if v < 250 then table.insert(tripList, v) end + end + for _, x in pairs(tripList) do + if foundIn(tripList, x + 2) and foundIn(tripList, x + 6) then + table.insert(triplets, "\n{" .. x .. "," .. x+2 .. "," .. x+6 .. "}") + end + end + show("First 25:", first25) + print(under1k .. " are less than or equal to 1000\n") + show("2000th to 2005th:", inRange) + show("Triplets:", triplets)`); + + assert.equal(code, 'Lua'); +}); + +test('lsp handler', () => { + const code = detectLang( + `--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion + M['textDocument/completion'] = function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local prefix = line_to_cursor:sub(textMatch+1) + + local matches = util.text_document_completion_list_to_complete_items(result, prefix) + vim.fn.complete(textMatch+1, matches) + end + `, + ); + assert.equal(code, 'Lua'); +}); + +test('yes, this is a valid lua code', () => { + const code = detectLang( + `local elements = u.namelist() + +[[element1]] { + bg = 0xffffff; + fg = nil; +} + +[[element2]] { + num = 123123; +} + +[[fooooooo]] { + ["false"] = true; + [0] = false; +}`, + ); + + assert.equal(code, 'Lua'); +}); + +test.run();