aboutsummaryrefslogtreecommitdiffstats
path: root/lsp/angularls.lua
blob: af82b87a1209f9b2ccfdcb4a87e82df5ff69b84d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
---@brief
---
--- https://github.com/angular/vscode-ng-language-service
--- `angular-language-server` can be installed via npm `npm install -g @angular/language-server`.
---
--- ```lua
--- local project_library_path = "/path/to/project/lib"
--- local cmd = {"ngserver", "--stdio", "--tsProbeLocations", project_library_path , "--ngProbeLocations", project_library_path}
---
--- vim.lsp.config('angularls', {
---   cmd = cmd,
--- })
--- ```

-- Angular requires a node_modules directory to probe for @angular/language-service and typescript
-- in order to use your projects configured versions.
local fs, fn, uv = vim.fs, vim.fn, vim.uv

--- Recursively solve for the original ngserver path on Windows
-- For a given ngserver path:
--   - If it is not a CMD wrapper, return the path;
--   - Or else, extract the path from the CMD wrapper.
--
-- @param cmd_path (string) path for the ngserver executable or its CMD wrapper.
-- @return (string) the original executable path for ngserver
-- @usage
-- -- Base case: cmd_path already points to ngserver (expected behavior on Linux)
-- resolve_cmd_shim('/home/user/project/node_modules/@angular/language-server/bin/ngserver')
-- => '/home/user/project/node_modules/@angular/language-server/bin/ngserver'
--
-- -- Recursive case: cmd_path points to a CMD wrapper (Windows)
-- resolve_cmd_shim('C:/Users/user/project/node_modules/.bin/ngserver.cmd')
-- => 'C:/Users/user/project/node_modules/@angular/language-server/bin/ngserver'
local function resolve_cmd_shim(cmd_path)
  if not cmd_path:lower():match('%ngserver.cmd$') then
    return cmd_path
  end

  local ok, content = pcall(fn.readblob, cmd_path)
  if not ok or not content then
    return cmd_path
  end

  local target = content:match('%s%"%%dp0%%\\([^\r\n]-ngserver[^\r\n]-)%"')
  if not target then
    return cmd_path
  end

  local full = fs.normalize(fs.joinpath(fs.dirname(cmd_path), target))

  return resolve_cmd_shim(full)
end

local function collect_node_modules(root_dir)
  local results = {}

  local project_node = fs.joinpath(root_dir, 'node_modules')
  if uv.fs_stat(project_node) then
    table.insert(results, project_node)
  end

  local ngserver_exe = fn.exepath('ngserver')
  if ngserver_exe and #ngserver_exe > 0 then
    local realpath = uv.fs_realpath(ngserver_exe) or ngserver_exe
    realpath = resolve_cmd_shim(realpath)
    local candidate = fs.normalize(fs.joinpath(fs.dirname(realpath), '../../..'))
    if uv.fs_stat(candidate) then
      table.insert(results, candidate)
    end
  end

  return results
end

local function get_angular_core_version(root_dir)
  local package_json = fs.joinpath(root_dir, 'package.json')
  if not uv.fs_stat(package_json) then
    return ''
  end

  local ok, content = pcall(fn.readblob, package_json)
  if not ok or not content then
    return ''
  end

  local json = vim.json.decode(content) or {}

  local version = (json.dependencies or {})['@angular/core'] or ''
  return version:match('%d+%.%d+%.%d+') or ''
end

---@type vim.lsp.Config
return {
  cmd = function(dispatchers, config)
    local root_dir = (config and config.root_dir) or fn.getcwd()
    local node_paths = collect_node_modules(root_dir)

    local ts_probe = table.concat(node_paths, ',')
    local ng_probe = table.concat(
      vim
        .iter(node_paths)
        :map(function(p)
          return fs.joinpath(p, '@angular/language-server/node_modules')
        end)
        :totable(),
      ','
    )
    local cmd = {
      'ngserver',
      '--stdio',
      '--tsProbeLocations',
      ts_probe,
      '--ngProbeLocations',
      ng_probe,
      '--angularCoreVersion',
      get_angular_core_version(root_dir),
    }
    return vim.lsp.rpc.start(cmd, dispatchers)
  end,

  filetypes = { 'typescript', 'html', 'typescriptreact', 'typescript.tsx', 'htmlangular' },
  root_markers = { 'angular.json', 'nx.json' },
}