neml2-langserv 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: neml2-langserv
3
+ Version: 0.1.1
4
+ Summary: Language server backing the NEML2 VS Code extension
5
+ Project-URL: Homepage, https://github.com/applied-material-modeling/neml2-langserv
6
+ Project-URL: Issues, https://github.com/applied-material-modeling/neml2-langserv/issues
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pygls>=1.3
10
+ Requires-Dist: neml2>=2.1.4
11
+ Requires-Dist: nmhit>=0.1.2
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: build; extra == "dev"
15
+
16
+ # NEML2 Language Support
17
+
18
+ VS Code extension providing language support for [NEML2](https://github.com/applied-material-modeling/neml2) input files.
19
+
20
+ ## Features
21
+
22
+ - **Completion** — type names for `type = ` assignments, and option names with inline type hints inside typed blocks
23
+ - **Hover documentation** — docstrings for types and options shown on hover
24
+ - **Format on save** — re-indents the document consistently via the `nmhit` formatter
25
+
26
+ ## Requirements
27
+
28
+ A Python environment with the [`neml2-langserv`](https://pypi.org/project/neml2-langserv/) package installed:
29
+
30
+ ```
31
+ pip install neml2-langserv
32
+ ```
33
+
34
+ That single install pulls in everything the language server needs — [`neml2`](https://pypi.org/project/neml2/) (≥ 2.1.4) for the type/option metadata, [`nmhit`](https://pypi.org/project/nmhit/) (≥ 0.1.2) for the formatter, and `pygls` for the LSP transport.
35
+
36
+ The extension runs the server with whichever Python interpreter is selected in the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python). If `neml2-langserv` is not installed in that interpreter on first activation, the extension prompts you to install it.
37
+
38
+ ## Setup
39
+
40
+ NEML2 input files share the `.i` extension with MOOSE input files. To tell the extension that a file is a NEML2 input, add the following as the **first line**:
41
+
42
+ ```
43
+ # neml2
44
+ ```
45
+
46
+ Files without this marker are treated as MOOSE input (or plain text) and the NEML2 language server will not activate for them.
47
+
48
+ ## Format on save
49
+
50
+ Enable the built-in VS Code setting to auto-format on save:
51
+
52
+ ```jsonc
53
+ // .vscode/settings.json
54
+ {
55
+ "[neml2]": {
56
+ "editor.formatOnSave": true
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## MOOSE compatibility
62
+
63
+ This extension does **not** claim the `.i` extension globally, so existing MOOSE workflows are unaffected. Only files whose first line matches `# neml2` (optionally followed by other text) are switched to the `neml2` language mode.
@@ -0,0 +1,48 @@
1
+ # NEML2 Language Support
2
+
3
+ VS Code extension providing language support for [NEML2](https://github.com/applied-material-modeling/neml2) input files.
4
+
5
+ ## Features
6
+
7
+ - **Completion** — type names for `type = ` assignments, and option names with inline type hints inside typed blocks
8
+ - **Hover documentation** — docstrings for types and options shown on hover
9
+ - **Format on save** — re-indents the document consistently via the `nmhit` formatter
10
+
11
+ ## Requirements
12
+
13
+ A Python environment with the [`neml2-langserv`](https://pypi.org/project/neml2-langserv/) package installed:
14
+
15
+ ```
16
+ pip install neml2-langserv
17
+ ```
18
+
19
+ That single install pulls in everything the language server needs — [`neml2`](https://pypi.org/project/neml2/) (≥ 2.1.4) for the type/option metadata, [`nmhit`](https://pypi.org/project/nmhit/) (≥ 0.1.2) for the formatter, and `pygls` for the LSP transport.
20
+
21
+ The extension runs the server with whichever Python interpreter is selected in the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python). If `neml2-langserv` is not installed in that interpreter on first activation, the extension prompts you to install it.
22
+
23
+ ## Setup
24
+
25
+ NEML2 input files share the `.i` extension with MOOSE input files. To tell the extension that a file is a NEML2 input, add the following as the **first line**:
26
+
27
+ ```
28
+ # neml2
29
+ ```
30
+
31
+ Files without this marker are treated as MOOSE input (or plain text) and the NEML2 language server will not activate for them.
32
+
33
+ ## Format on save
34
+
35
+ Enable the built-in VS Code setting to auto-format on save:
36
+
37
+ ```jsonc
38
+ // .vscode/settings.json
39
+ {
40
+ "[neml2]": {
41
+ "editor.formatOnSave": true
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## MOOSE compatibility
47
+
48
+ This extension does **not** claim the `.i` extension globally, so existing MOOSE workflows are unaffected. Only files whose first line matches `# neml2` (optionally followed by other text) are switched to the `neml2` language mode.
File without changes
@@ -0,0 +1,19 @@
1
+ import argparse
2
+
3
+ from .server import server
4
+
5
+
6
+ def main() -> None:
7
+ parser = argparse.ArgumentParser(description="NEML2 Language Server")
8
+ parser.add_argument("--stdio", action="store_true", help="use stdio transport")
9
+ parser.add_argument("--tcp", type=int, metavar="PORT", help="use TCP transport on PORT")
10
+ args = parser.parse_args()
11
+
12
+ if args.tcp:
13
+ server.start_tcp("127.0.0.1", args.tcp)
14
+ else:
15
+ server.start_io()
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,129 @@
1
+ """Tolerant HIT document context parser.
2
+
3
+ Uses only bracket delimiters ([name] / []) — no indentation logic.
4
+ Bracket depth: 0 = file root, 1 = top-level section, 2 = sub-block.
5
+ """
6
+
7
+ import re
8
+ from dataclasses import dataclass, field
9
+
10
+ _OPEN = re.compile(r"^\s*\[([^\]/][^\]]*)\]\s*(?:#.*)?$")
11
+ _CLOSE = re.compile(r"^\s*\[\]\s*(?:#.*)?$")
12
+ _KV = re.compile(r"^\s*(\w[\w/]*)\s*[:=]=?\s*(.*?)\s*(?:#.*)?$")
13
+
14
+
15
+ @dataclass
16
+ class BlockContext:
17
+ section: str = ""
18
+ block_name: str = ""
19
+ block_type: str = ""
20
+ options_set: set[str] = field(default_factory=set)
21
+
22
+
23
+ def get_context(lines: list[str], cursor_line: int) -> BlockContext | None:
24
+ """Return the HIT context at *cursor_line* (0-indexed)."""
25
+ depth = 0
26
+ ctx = BlockContext()
27
+
28
+ for i, line in enumerate(lines):
29
+ if i > cursor_line:
30
+ break
31
+
32
+ close = _CLOSE.match(line)
33
+ if close:
34
+ if depth == 2:
35
+ ctx.block_name = ""
36
+ ctx.block_type = ""
37
+ ctx.options_set = set()
38
+ if depth == 1:
39
+ ctx.section = ""
40
+ depth = max(0, depth - 1)
41
+ continue
42
+
43
+ open_m = _OPEN.match(line)
44
+ if open_m:
45
+ name = open_m.group(1).strip()
46
+ if depth == 0:
47
+ ctx.section = name
48
+ depth = 1
49
+ elif depth == 1:
50
+ ctx.block_name = name
51
+ ctx.block_type = ""
52
+ ctx.options_set = set()
53
+ depth = 2
54
+ continue
55
+
56
+ if depth == 2:
57
+ kv = _KV.match(line)
58
+ if kv:
59
+ key, val = kv.group(1), kv.group(2).strip()
60
+ if key == "type":
61
+ ctx.block_type = val.strip("'\"")
62
+ else:
63
+ ctx.options_set.add(key)
64
+
65
+ if depth < 2:
66
+ return None
67
+ return ctx
68
+
69
+
70
+ @dataclass
71
+ class ParsedBlock:
72
+ context: BlockContext
73
+ start_line: int
74
+ end_line: int
75
+
76
+
77
+ def parse_all_blocks(lines: list[str]) -> list[ParsedBlock]:
78
+ """Return every sub-block found in the document."""
79
+ blocks: list[ParsedBlock] = []
80
+ depth = 0
81
+ section = ""
82
+ block_name = ""
83
+ block_type = ""
84
+ options_set: set[str] = set()
85
+ block_start = 0
86
+
87
+ for i, line in enumerate(lines):
88
+ close = _CLOSE.match(line)
89
+ if close:
90
+ if depth == 2:
91
+ blocks.append(
92
+ ParsedBlock(
93
+ BlockContext(section, block_name, block_type, options_set),
94
+ block_start,
95
+ i,
96
+ )
97
+ )
98
+ block_name = ""
99
+ block_type = ""
100
+ options_set = set()
101
+ elif depth == 1:
102
+ section = ""
103
+ depth = max(0, depth - 1)
104
+ continue
105
+
106
+ open_m = _OPEN.match(line)
107
+ if open_m:
108
+ name = open_m.group(1).strip()
109
+ if depth == 0:
110
+ section = name
111
+ depth = 1
112
+ elif depth == 1:
113
+ block_name = name
114
+ block_type = ""
115
+ options_set = set()
116
+ block_start = i
117
+ depth = 2
118
+ continue
119
+
120
+ if depth == 2:
121
+ kv = _KV.match(line)
122
+ if kv:
123
+ key, val = kv.group(1), kv.group(2).strip()
124
+ if key == "type":
125
+ block_type = val.strip("'\"")
126
+ else:
127
+ options_set.add(key)
128
+
129
+ return blocks
@@ -0,0 +1,240 @@
1
+ import re
2
+ from importlib.metadata import PackageNotFoundError
3
+ from importlib.metadata import version as _pkg_version
4
+
5
+ import nmhit
6
+ from lsprotocol import types as lsp
7
+ from pygls.lsp.server import LanguageServer
8
+
9
+ from .hit_parser import get_context, parse_all_blocks
10
+ from .syntax_client import NEML2_MIN_VERSION, NMHIT_MIN_VERSION, get_client
11
+
12
+ server = LanguageServer("neml2-ls", "v0.1")
13
+
14
+ def _version_tuple(v: str) -> tuple[int, ...]:
15
+ return tuple(int(x) for x in v.split(".")[:3])
16
+
17
+
18
+ def _check_pkg(pkg: str, min_ver: str) -> str | None:
19
+ """Return a warning string if pkg is missing or below min_ver, else None."""
20
+ try:
21
+ installed = _pkg_version(pkg)
22
+ if _version_tuple(installed) < _version_tuple(min_ver):
23
+ return (
24
+ f"'{pkg}' {installed} is installed but >={min_ver} is required. "
25
+ "Some features may not work correctly."
26
+ )
27
+ except PackageNotFoundError:
28
+ return (
29
+ f"'{pkg}' is not installed (>={min_ver} required). "
30
+ "Please install it in the active Python environment."
31
+ )
32
+ return None
33
+
34
+
35
+ @server.feature(lsp.INITIALIZED)
36
+ def _on_initialized(ls: LanguageServer, params: lsp.InitializedParams) -> None:
37
+ for pkg, min_ver, severity in [
38
+ ("neml2", NEML2_MIN_VERSION, lsp.MessageType.Error),
39
+ ("nmhit", NMHIT_MIN_VERSION, lsp.MessageType.Warning),
40
+ ]:
41
+ msg = _check_pkg(pkg, min_ver)
42
+ if msg:
43
+ ls.window_show_message(
44
+ lsp.ShowMessageParams(type=severity, message=f"NEML2: {msg}")
45
+ )
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # helpers
49
+ # ---------------------------------------------------------------------------
50
+
51
+ _FTYPE_KIND = {
52
+ "PARAMETER": lsp.CompletionItemKind.Variable,
53
+ "INPUT": lsp.CompletionItemKind.Field,
54
+ "OUTPUT": lsp.CompletionItemKind.Field,
55
+ "BUFFER": lsp.CompletionItemKind.Variable,
56
+ "NONE": lsp.CompletionItemKind.Property,
57
+ }
58
+
59
+ _TYPE_ASSIGN = re.compile(r"^\s*type\s*[:=]=?\s*\S*$")
60
+ _KEY_POS = re.compile(r"^\s*[\w/]*$")
61
+
62
+
63
+ def _doc_lines(lines: list[str]) -> list[str]:
64
+ """Return document lines (handles both list[str] and str)."""
65
+ return lines
66
+
67
+
68
+ def _word_at(line: str, character: int) -> str:
69
+ start = character
70
+ while start > 0 and (line[start - 1].isalnum() or line[start - 1] in "_:/"):
71
+ start -= 1
72
+ end = character
73
+ while end < len(line) and (line[end].isalnum() or line[end] in "_:/"):
74
+ end += 1
75
+ return line[start:end]
76
+
77
+
78
+ def _fmt_option(opt: dict) -> str:
79
+ ftype = opt.get("ftype", "")
80
+ header = f"**{opt['name']}**" + (f" `{ftype}`" if ftype and ftype != "NONE" else "")
81
+ lines = [header]
82
+ if opt.get("type"):
83
+ lines.append(f"*Type:* `{opt['type']}`")
84
+ if opt.get("required"):
85
+ lines.append("*Required*")
86
+ if opt.get("doc"):
87
+ lines.append("")
88
+ lines.append(opt["doc"])
89
+ return "\n".join(lines)
90
+
91
+
92
+ def _get_lines(ls: LanguageServer, uri: str) -> list[str]:
93
+ doc = ls.workspace.get_text_document(uri)
94
+ return doc.source.splitlines()
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # completion
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ @server.feature(
103
+ lsp.TEXT_DOCUMENT_COMPLETION,
104
+ lsp.CompletionOptions(trigger_characters=["=", " ", "\t"]),
105
+ )
106
+ def completions(
107
+ ls: LanguageServer, params: lsp.CompletionParams
108
+ ) -> list[lsp.CompletionItem]:
109
+ lines = _get_lines(ls, params.text_document.uri)
110
+ line_idx = params.position.line
111
+ if line_idx >= len(lines):
112
+ return []
113
+
114
+ current_line = lines[line_idx]
115
+ ctx = get_context(lines, line_idx)
116
+ if ctx is None:
117
+ return []
118
+
119
+ syntax = get_client()
120
+ if syntax is None:
121
+ return []
122
+
123
+ # After `type =` → offer all registered types for this section
124
+ if _TYPE_ASSIGN.match(current_line):
125
+ try:
126
+ types = syntax.list_types(ctx.section)
127
+ except Exception:
128
+ return []
129
+ return [
130
+ lsp.CompletionItem(
131
+ label=t["type"],
132
+ kind=lsp.CompletionItemKind.Class,
133
+ detail=t["section"],
134
+ documentation=lsp.MarkupContent(
135
+ kind=lsp.MarkupKind.PlainText, value=t.get("doc", "")
136
+ ),
137
+ )
138
+ for t in types
139
+ ]
140
+
141
+ # At an option key position → offer option names for the current type
142
+ if ctx.block_type and _KEY_POS.match(current_line):
143
+ try:
144
+ info = syntax.get_options(ctx.block_type)
145
+ except Exception:
146
+ return []
147
+ if not info:
148
+ return []
149
+ remaining = [o for o in info["options"] if o["name"] not in ctx.options_set]
150
+ return [
151
+ lsp.CompletionItem(
152
+ label=o["name"],
153
+ kind=_FTYPE_KIND.get(o["ftype"], lsp.CompletionItemKind.Property),
154
+ detail=o.get("type") or "",
155
+ documentation=lsp.MarkupContent(
156
+ kind=lsp.MarkupKind.Markdown, value=_fmt_option(o)
157
+ ),
158
+ )
159
+ for o in remaining
160
+ ]
161
+
162
+ return []
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # hover
167
+ # ---------------------------------------------------------------------------
168
+
169
+
170
+ @server.feature(lsp.TEXT_DOCUMENT_HOVER)
171
+ def hover(ls: LanguageServer, params: lsp.HoverParams) -> lsp.Hover | None:
172
+ lines = _get_lines(ls, params.text_document.uri)
173
+ line_idx = params.position.line
174
+ if line_idx >= len(lines):
175
+ return None
176
+
177
+ word = _word_at(lines[line_idx], params.position.character)
178
+ if not word:
179
+ return None
180
+
181
+ ctx = get_context(lines, line_idx)
182
+ syntax = get_client()
183
+ if syntax is None:
184
+ return None
185
+
186
+ # Hover on a type name
187
+ try:
188
+ info = syntax.get_options(word)
189
+ if info:
190
+ md = f"**{word}** _{info['section']}_\n\n{info.get('doc', '')}"
191
+ return lsp.Hover(
192
+ contents=lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=md)
193
+ )
194
+ except Exception:
195
+ pass
196
+
197
+ # Hover on an option key within a typed block
198
+ if ctx and ctx.block_type:
199
+ try:
200
+ info = syntax.get_options(ctx.block_type)
201
+ if info:
202
+ by_name = {o["name"]: o for o in info["options"]}
203
+ if word in by_name:
204
+ return lsp.Hover(
205
+ contents=lsp.MarkupContent(
206
+ kind=lsp.MarkupKind.Markdown,
207
+ value=_fmt_option(by_name[word]),
208
+ )
209
+ )
210
+ except Exception:
211
+ pass
212
+
213
+ return None
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # formatting
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ @server.feature(lsp.TEXT_DOCUMENT_FORMATTING)
222
+ def formatting(
223
+ ls: LanguageServer, params: lsp.DocumentFormattingParams
224
+ ) -> list[lsp.TextEdit] | None:
225
+ doc = ls.workspace.get_text_document(params.text_document.uri)
226
+ source = doc.source
227
+ try:
228
+ root = nmhit.parse_text(source)
229
+ formatted = root.render(indent=0, indent_text=" ")
230
+ except Exception:
231
+ return None
232
+
233
+ lines = source.splitlines()
234
+ last_line = len(lines) - 1
235
+ last_char = len(lines[last_line]) if lines else 0
236
+ whole_doc = lsp.Range(
237
+ start=lsp.Position(line=0, character=0),
238
+ end=lsp.Position(line=last_line, character=last_char),
239
+ )
240
+ return [lsp.TextEdit(range=whole_doc, new_text=formatted)]
@@ -0,0 +1,93 @@
1
+ import atexit
2
+ import json
3
+ import subprocess
4
+ import threading
5
+ from importlib.metadata import PackageNotFoundError
6
+ from importlib.metadata import version as _pkg_version
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ NEML2_MIN_VERSION = "2.1.4"
12
+ NMHIT_MIN_VERSION = "0.1.2"
13
+
14
+
15
+ def _neml2_ok() -> bool:
16
+ try:
17
+ v = _pkg_version("neml2")
18
+ min_t = tuple(int(x) for x in NEML2_MIN_VERSION.split(".")[:3])
19
+ return tuple(int(x) for x in v.split(".")[:3]) >= min_t
20
+ except PackageNotFoundError:
21
+ return False
22
+
23
+
24
+ def _find_binary() -> Path:
25
+ """Locate the neml2-syntax binary bundled with the neml2 Python package."""
26
+ for pkg_dir in neml2.__path__:
27
+ candidate = Path(pkg_dir) / "bin" / "neml2-syntax"
28
+ if candidate.exists():
29
+ return candidate
30
+ searched = [str(Path(p) / "bin" / "neml2-syntax") for p in neml2.__path__]
31
+ raise RuntimeError(f"neml2-syntax not found; searched: {searched}")
32
+
33
+
34
+ class SyntaxClient:
35
+ """Long-lived wrapper around `neml2-syntax --server`."""
36
+
37
+ def __init__(self) -> None:
38
+ exe = _find_binary()
39
+ self._proc = subprocess.Popen(
40
+ [str(exe), "--server"],
41
+ stdin=subprocess.PIPE,
42
+ stdout=subprocess.PIPE,
43
+ stderr=subprocess.DEVNULL,
44
+ text=True,
45
+ bufsize=1,
46
+ )
47
+ self._lock = threading.Lock()
48
+ self._next_id = 1
49
+ atexit.register(self.close)
50
+
51
+ def _request(self, method: str, **params: Any) -> Any:
52
+ with self._lock:
53
+ req_id = self._next_id
54
+ self._next_id += 1
55
+ payload = {"id": req_id, "method": method, **params}
56
+ assert self._proc.stdin is not None
57
+ assert self._proc.stdout is not None
58
+ self._proc.stdin.write(json.dumps(payload) + "\n")
59
+ self._proc.stdin.flush()
60
+ line = self._proc.stdout.readline()
61
+ resp = json.loads(line)
62
+ if "error" in resp:
63
+ raise RuntimeError(f"neml2-syntax error: {resp['error']}")
64
+ return resp["result"]
65
+
66
+ def list_sections(self) -> list[str]:
67
+ return self._request("list_sections")
68
+
69
+ def list_types(self, section: str = "") -> list[dict]:
70
+ return self._request("list_types", section=section)
71
+
72
+ def get_options(self, type_name: str) -> dict | None:
73
+ return self._request("get_options", type=type_name)
74
+
75
+ def close(self) -> None:
76
+ if self._proc.poll() is None:
77
+ self._proc.terminate()
78
+ try:
79
+ self._proc.wait(timeout=2)
80
+ except subprocess.TimeoutExpired:
81
+ self._proc.kill()
82
+
83
+
84
+ _client: SyntaxClient | None = None
85
+
86
+
87
+ def get_client() -> SyntaxClient | None:
88
+ if not _neml2_ok():
89
+ return None
90
+ global _client
91
+ if _client is None:
92
+ _client = SyntaxClient()
93
+ return _client
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: neml2-langserv
3
+ Version: 0.1.1
4
+ Summary: Language server backing the NEML2 VS Code extension
5
+ Project-URL: Homepage, https://github.com/applied-material-modeling/neml2-langserv
6
+ Project-URL: Issues, https://github.com/applied-material-modeling/neml2-langserv/issues
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pygls>=1.3
10
+ Requires-Dist: neml2>=2.1.4
11
+ Requires-Dist: nmhit>=0.1.2
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: build; extra == "dev"
15
+
16
+ # NEML2 Language Support
17
+
18
+ VS Code extension providing language support for [NEML2](https://github.com/applied-material-modeling/neml2) input files.
19
+
20
+ ## Features
21
+
22
+ - **Completion** — type names for `type = ` assignments, and option names with inline type hints inside typed blocks
23
+ - **Hover documentation** — docstrings for types and options shown on hover
24
+ - **Format on save** — re-indents the document consistently via the `nmhit` formatter
25
+
26
+ ## Requirements
27
+
28
+ A Python environment with the [`neml2-langserv`](https://pypi.org/project/neml2-langserv/) package installed:
29
+
30
+ ```
31
+ pip install neml2-langserv
32
+ ```
33
+
34
+ That single install pulls in everything the language server needs — [`neml2`](https://pypi.org/project/neml2/) (≥ 2.1.4) for the type/option metadata, [`nmhit`](https://pypi.org/project/nmhit/) (≥ 0.1.2) for the formatter, and `pygls` for the LSP transport.
35
+
36
+ The extension runs the server with whichever Python interpreter is selected in the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python). If `neml2-langserv` is not installed in that interpreter on first activation, the extension prompts you to install it.
37
+
38
+ ## Setup
39
+
40
+ NEML2 input files share the `.i` extension with MOOSE input files. To tell the extension that a file is a NEML2 input, add the following as the **first line**:
41
+
42
+ ```
43
+ # neml2
44
+ ```
45
+
46
+ Files without this marker are treated as MOOSE input (or plain text) and the NEML2 language server will not activate for them.
47
+
48
+ ## Format on save
49
+
50
+ Enable the built-in VS Code setting to auto-format on save:
51
+
52
+ ```jsonc
53
+ // .vscode/settings.json
54
+ {
55
+ "[neml2]": {
56
+ "editor.formatOnSave": true
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## MOOSE compatibility
62
+
63
+ This extension does **not** claim the `.i` extension globally, so existing MOOSE workflows are unaffected. Only files whose first line matches `# neml2` (optionally followed by other text) are switched to the `neml2` language mode.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ neml2_langserv/__init__.py
4
+ neml2_langserv/__main__.py
5
+ neml2_langserv/hit_parser.py
6
+ neml2_langserv/server.py
7
+ neml2_langserv/syntax_client.py
8
+ neml2_langserv.egg-info/PKG-INFO
9
+ neml2_langserv.egg-info/SOURCES.txt
10
+ neml2_langserv.egg-info/dependency_links.txt
11
+ neml2_langserv.egg-info/requires.txt
12
+ neml2_langserv.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ pygls>=1.3
2
+ neml2>=2.1.4
3
+ nmhit>=0.1.2
4
+
5
+ [dev]
6
+ pytest
7
+ build
@@ -0,0 +1 @@
1
+ neml2_langserv
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "neml2-langserv"
7
+ version = "0.1.1"
8
+ description = "Language server backing the NEML2 VS Code extension"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "pygls>=1.3",
13
+ "neml2>=2.1.4",
14
+ "nmhit>=0.1.2",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest", "build"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/applied-material-modeling/neml2-langserv"
22
+ Issues = "https://github.com/applied-material-modeling/neml2-langserv/issues"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
26
+ include = ["neml2_langserv*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+