nou-cli 0.1.0 → 0.2.0
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.
- package/README.md +140 -31
- package/dist/grammars/tree-sitter-bash.wasm +0 -0
- package/dist/grammars/tree-sitter-c.wasm +0 -0
- package/dist/grammars/tree-sitter-css.wasm +0 -0
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-html.wasm +0 -0
- package/dist/grammars/tree-sitter-java.wasm +0 -0
- package/dist/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/grammars/tree-sitter-json.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/grammars/tree-sitter-toml.wasm +0 -0
- package/dist/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/grammars/tree-sitter-yaml.wasm +0 -0
- package/dist/index.js +55 -6
- package/dist/src/agent/index.js +37 -5
- package/dist/src/commands/index.js +16 -6
- package/dist/src/config/index.js +5 -2
- package/dist/src/diagnostics/index.js +235 -0
- package/dist/src/diagnostics/process.js +64 -0
- package/dist/src/diagnostics/providers.js +103 -0
- package/dist/src/diagnostics/types.js +1 -0
- package/dist/src/env.js +12 -0
- package/dist/src/history/index.js +113 -0
- package/dist/src/lsp/client.js +169 -0
- package/dist/src/lsp/index.js +169 -0
- package/dist/src/plugins/index.js +94 -3
- package/dist/src/providers/ids.js +3 -0
- package/dist/src/providers/ollama.js +2 -1
- package/dist/src/providers/openai.js +3 -2
- package/dist/src/syntax/index.js +240 -0
- package/dist/src/ui/App.js +268 -29
- package/dist/src/ui/Chat.js +11 -4
- package/dist/src/ui/ChatInput.js +128 -0
- package/dist/src/ui/CodeView.js +21 -5
- package/dist/src/ui/Editor.js +90 -21
- package/dist/src/ui/FileTree.js +6 -4
- package/dist/src/ui/Header.js +2 -2
- package/dist/src/ui/ProblemsView.js +71 -0
- package/dist/src/ui/Settings.js +30 -15
- package/dist/src/ui/text.js +6 -1
- package/dist/src/update/index.js +102 -0
- package/package.json +9 -4
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
7
|
+
/** File extension -> grammar name (tree-sitter-<name>.wasm). Only these are shipped with Nou. */
|
|
8
|
+
const GRAMMARS = {
|
|
9
|
+
".ts": "typescript", ".mts": "typescript", ".cts": "typescript", ".tsx": "tsx",
|
|
10
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
11
|
+
".py": "python", ".pyi": "python", ".json": "json", ".css": "css", ".html": "html", ".htm": "html",
|
|
12
|
+
".sh": "bash", ".bash": "bash", ".go": "go", ".rs": "rust", ".java": "java", ".c": "c", ".h": "c",
|
|
13
|
+
".yml": "yaml", ".yaml": "yaml", ".toml": "toml",
|
|
14
|
+
};
|
|
15
|
+
export const SHIPPED_GRAMMARS = [...new Set(Object.values(GRAMMARS))];
|
|
16
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
/** Shipped builds keep grammars in dist/grammars; from source they come from the dev dependency. */
|
|
18
|
+
function grammarDir() {
|
|
19
|
+
const candidates = [path.join(here, "..", "..", "grammars"), path.join(here, "..", "..", "..", "grammars")];
|
|
20
|
+
try {
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
candidates.push(path.join(path.dirname(require.resolve("tree-sitter-wasms/package.json")), "out"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// not installed (normal for published builds)
|
|
26
|
+
}
|
|
27
|
+
return candidates.find((dir) => existsSync(path.join(dir, "tree-sitter-json.wasm"))) ?? null;
|
|
28
|
+
}
|
|
29
|
+
let init = null;
|
|
30
|
+
const languages = new Map();
|
|
31
|
+
let parser = null;
|
|
32
|
+
const listeners = new Set();
|
|
33
|
+
/** Called when a grammar finishes loading, so views can re-highlight. */
|
|
34
|
+
export function onGrammarLoaded(listener) {
|
|
35
|
+
listeners.add(listener);
|
|
36
|
+
return () => listeners.delete(listener);
|
|
37
|
+
}
|
|
38
|
+
function grammarFor(file) {
|
|
39
|
+
return GRAMMARS[path.extname(file).toLowerCase()];
|
|
40
|
+
}
|
|
41
|
+
function load(name) {
|
|
42
|
+
if (languages.has(name))
|
|
43
|
+
return;
|
|
44
|
+
languages.set(name, "loading");
|
|
45
|
+
init ??= Parser.init().then(() => {
|
|
46
|
+
parser = new Parser();
|
|
47
|
+
});
|
|
48
|
+
void init
|
|
49
|
+
.then(async () => {
|
|
50
|
+
const dir = grammarDir();
|
|
51
|
+
if (!dir)
|
|
52
|
+
throw new Error("grammars not found");
|
|
53
|
+
languages.set(name, await Language.load(path.join(dir, `tree-sitter-${name}.wasm`)));
|
|
54
|
+
for (const l of listeners)
|
|
55
|
+
l();
|
|
56
|
+
})
|
|
57
|
+
.catch(() => languages.set(name, "failed"));
|
|
58
|
+
}
|
|
59
|
+
/** Parses the file if its grammar is ready; otherwise starts loading it and returns null. */
|
|
60
|
+
function parse(file, text) {
|
|
61
|
+
const name = grammarFor(file);
|
|
62
|
+
if (!name)
|
|
63
|
+
return null;
|
|
64
|
+
const lang = languages.get(name);
|
|
65
|
+
if (!lang)
|
|
66
|
+
load(name);
|
|
67
|
+
if (!lang || typeof lang === "string" || !parser)
|
|
68
|
+
return null;
|
|
69
|
+
parser.setLanguage(lang);
|
|
70
|
+
return parser.parse(text);
|
|
71
|
+
}
|
|
72
|
+
export function hasGrammar(file) {
|
|
73
|
+
return grammarFor(file) !== undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Starts loading a file's grammar ahead of time. */
|
|
76
|
+
export function preload(file) {
|
|
77
|
+
const name = grammarFor(file);
|
|
78
|
+
if (name)
|
|
79
|
+
load(name);
|
|
80
|
+
}
|
|
81
|
+
// --- highlighting --------------------------------------------------------------
|
|
82
|
+
const COLORS = {
|
|
83
|
+
keyword: chalk.hex("#bb9af7"),
|
|
84
|
+
string: chalk.hex("#9ece6a"),
|
|
85
|
+
number: chalk.hex("#ff9e64"),
|
|
86
|
+
comment: chalk.hex("#565f89").italic,
|
|
87
|
+
type: chalk.hex("#2ac3de"),
|
|
88
|
+
func: chalk.hex("#7aa2f7"),
|
|
89
|
+
property: chalk.hex("#73daca"),
|
|
90
|
+
constant: chalk.hex("#ff9e64"),
|
|
91
|
+
tag: chalk.hex("#f7768e"),
|
|
92
|
+
punctuation: chalk.hex("#89ddff"),
|
|
93
|
+
plain: chalk.hex("#c0caf5"),
|
|
94
|
+
};
|
|
95
|
+
const STRINGS = /^(string|template_string|string_fragment|string_content|raw_string_literal|interpreted_string_literal|char_literal|character_literal|escape_sequence|string_literal|heredoc_body)$/;
|
|
96
|
+
const NUMBERS = /^(number|integer|float|integer_literal|float_literal|number_literal|decimal_integer_literal|decimal_floating_point_literal)$/;
|
|
97
|
+
const TYPES = /^(type_identifier|predefined_type|primitive_type|builtin_type|type)$/;
|
|
98
|
+
const PROPERTIES = /^(property_identifier|field_identifier|shorthand_property_identifier|shorthand_property_identifier_pattern|property_name|key|bare_key)$/;
|
|
99
|
+
const CONSTANTS = /^(true|false|null|undefined|none|nil|this|self|super|True|False|None|boolean)$/;
|
|
100
|
+
const FUNCTION_PARENTS = /^(function_declaration|function_definition|method_definition|function_item|method_declaration|function_signature|generator_function_declaration)$/;
|
|
101
|
+
const CALLS = /^(call_expression|call|method_invocation|new_expression)$/;
|
|
102
|
+
/** A named string node (anonymous keyword nodes can also be called "string", e.g. the TS type). */
|
|
103
|
+
const isString = (n) => n.isNamed && STRINGS.test(n.type);
|
|
104
|
+
const hasSubstitution = (n) => n.children.some((c) => c !== null && /substitution|interpolation/.test(c.type));
|
|
105
|
+
function colorFor(node) {
|
|
106
|
+
const type = node.type;
|
|
107
|
+
if (type.includes("comment"))
|
|
108
|
+
return "comment";
|
|
109
|
+
// Inside a string (but not inside ${...} substitutions)?
|
|
110
|
+
for (let p = node; p; p = p.parent) {
|
|
111
|
+
if (/substitution|interpolation/.test(p.type))
|
|
112
|
+
break;
|
|
113
|
+
if (isString(p))
|
|
114
|
+
return "string";
|
|
115
|
+
}
|
|
116
|
+
// Built-in type names (`number`, `string`) and Python annotations (`name: str`).
|
|
117
|
+
if (node.parent?.type === "predefined_type" || (node.parent?.type === "type" && /identifier$/.test(type)))
|
|
118
|
+
return "type";
|
|
119
|
+
if (NUMBERS.test(type))
|
|
120
|
+
return "number";
|
|
121
|
+
if (CONSTANTS.test(type) || CONSTANTS.test(node.text))
|
|
122
|
+
return node.isNamed || /^[a-zA-Z]+$/.test(node.text) ? "constant" : null;
|
|
123
|
+
if (TYPES.test(type) && (node.isNamed || node.parent?.type === "predefined_type"))
|
|
124
|
+
return "type";
|
|
125
|
+
if (/identifier$/.test(type)) {
|
|
126
|
+
const parent = node.parent;
|
|
127
|
+
if (parent && FUNCTION_PARENTS.test(parent.type) && parent.childForFieldName("name")?.equals(node))
|
|
128
|
+
return "func";
|
|
129
|
+
if (parent && CALLS.test(parent.type) && parent.childForFieldName("function")?.equals(node))
|
|
130
|
+
return "func";
|
|
131
|
+
// obj.method(): only the method name, not the object.
|
|
132
|
+
const member = parent && /member_expression|attribute|field_expression|selector_expression/.test(parent.type)
|
|
133
|
+
? parent.childForFieldName("property") ?? parent.childForFieldName("attribute") ?? parent.childForFieldName("field")
|
|
134
|
+
: null;
|
|
135
|
+
if (member?.equals(node) && parent.parent && CALLS.test(parent.parent.type) && parent.parent.childForFieldName("function")?.equals(parent))
|
|
136
|
+
return "func";
|
|
137
|
+
}
|
|
138
|
+
if (PROPERTIES.test(type))
|
|
139
|
+
return "property";
|
|
140
|
+
if (/tag_name|jsx_opening_element|jsx_closing_element/.test(type))
|
|
141
|
+
return "tag";
|
|
142
|
+
if (type === "identifier") {
|
|
143
|
+
if (/^[A-Z][A-Z0-9_]+$/.test(node.text))
|
|
144
|
+
return "constant";
|
|
145
|
+
if (/^[A-Z]/.test(node.text))
|
|
146
|
+
return "type";
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (!node.isNamed) {
|
|
150
|
+
if (node.parent?.type === "predefined_type")
|
|
151
|
+
return "type";
|
|
152
|
+
return /^[a-z_]+$/i.test(type) ? "keyword" : "punctuation";
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/** Syntax-highlights `text` with tree-sitter, one ANSI string per line; null if the grammar isn't ready. */
|
|
157
|
+
export function highlightTree(file, text) {
|
|
158
|
+
if (text.length > 300_000)
|
|
159
|
+
return null;
|
|
160
|
+
const tree = parse(file, text);
|
|
161
|
+
if (!tree)
|
|
162
|
+
return null;
|
|
163
|
+
let out = "";
|
|
164
|
+
let pos = 0;
|
|
165
|
+
const cursor = tree.walk();
|
|
166
|
+
// Depth-first over leaves, coloring each span.
|
|
167
|
+
const visit = () => {
|
|
168
|
+
do {
|
|
169
|
+
const node = cursor.currentNode;
|
|
170
|
+
if (node.childCount === 0 || node.type.includes("comment") || (isString(node) && !hasSubstitution(node))) {
|
|
171
|
+
if (node.startIndex >= pos) {
|
|
172
|
+
out += COLORS.plain(text.slice(pos, node.startIndex));
|
|
173
|
+
const color = colorFor(node);
|
|
174
|
+
const span = text.slice(node.startIndex, node.endIndex);
|
|
175
|
+
// Color each line separately so styles don't bleed across line breaks.
|
|
176
|
+
out += span.split("\n").map((part) => (color ? COLORS[color](part) : COLORS.plain(part))).join("\n");
|
|
177
|
+
pos = node.endIndex;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (cursor.gotoFirstChild()) {
|
|
181
|
+
visit();
|
|
182
|
+
cursor.gotoParent();
|
|
183
|
+
}
|
|
184
|
+
} while (cursor.gotoNextSibling());
|
|
185
|
+
};
|
|
186
|
+
if (cursor.gotoFirstChild())
|
|
187
|
+
visit();
|
|
188
|
+
out += COLORS.plain(text.slice(pos));
|
|
189
|
+
tree.delete();
|
|
190
|
+
return out.split("\n");
|
|
191
|
+
}
|
|
192
|
+
// --- syntax errors -------------------------------------------------------------
|
|
193
|
+
/** Syntax errors straight from the parser: instant, and works without any linter installed. */
|
|
194
|
+
export const syntaxProvider = {
|
|
195
|
+
id: "syntax",
|
|
196
|
+
name: "Syntax",
|
|
197
|
+
checksText: true,
|
|
198
|
+
matches: hasGrammar,
|
|
199
|
+
available: () => grammarDir() !== null,
|
|
200
|
+
async check(file, text) {
|
|
201
|
+
preload(file);
|
|
202
|
+
await init;
|
|
203
|
+
// Wait briefly for the grammar on first use.
|
|
204
|
+
for (let i = 0; i < 50 && languages.get(grammarFor(file)) === "loading"; i++)
|
|
205
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
206
|
+
const tree = parse(file, text);
|
|
207
|
+
if (!tree || !tree.rootNode.hasError) {
|
|
208
|
+
tree?.delete();
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const found = [];
|
|
212
|
+
const rel = path.relative(process.cwd(), path.resolve(process.cwd(), file)).replace(/\\/g, "/");
|
|
213
|
+
const visit = (node) => {
|
|
214
|
+
if (found.length >= 20)
|
|
215
|
+
return;
|
|
216
|
+
if (node.type === "ERROR" || node.isMissing) {
|
|
217
|
+
const snippet = node.text.split("\n")[0].slice(0, 30);
|
|
218
|
+
found.push({
|
|
219
|
+
file: rel,
|
|
220
|
+
line: node.startPosition.row + 1,
|
|
221
|
+
col: node.startPosition.column + 1,
|
|
222
|
+
endLine: node.endPosition.row + 1,
|
|
223
|
+
endCol: node.endPosition.column + 1,
|
|
224
|
+
severity: "error",
|
|
225
|
+
message: node.isMissing ? `Syntax error: missing "${node.type}"` : `Syntax error near ${JSON.stringify(snippet)}`,
|
|
226
|
+
source: "syntax",
|
|
227
|
+
});
|
|
228
|
+
if (node.type === "ERROR")
|
|
229
|
+
return; // don't report nested errors twice
|
|
230
|
+
}
|
|
231
|
+
if (node.hasError)
|
|
232
|
+
for (const child of node.children)
|
|
233
|
+
if (child)
|
|
234
|
+
visit(child);
|
|
235
|
+
};
|
|
236
|
+
visit(tree.rootNode);
|
|
237
|
+
tree.delete();
|
|
238
|
+
return found;
|
|
239
|
+
},
|
|
240
|
+
};
|