pi-revit 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/extensions/pi-revit/index.ts +239 -0
- package/package.json +28 -0
- package/scripts/build.ps1 +37 -0
- package/scripts/deploy.ps1 +99 -0
- package/scripts/revit.ps1 +112 -0
- package/scripts/setup-workspace.ps1 +53 -0
- package/scripts/uninstall.ps1 +103 -0
- package/skills/pi-revit/SKILL.md +50 -0
- package/src/Revit/Application.cs +94 -0
- package/src/Revit/BridgeServer.cs +419 -0
- package/src/Revit/CommandQueue.cs +93 -0
- package/src/Revit/RevitBridge.addin +11 -0
- package/src/Revit/RevitBridge.csproj +48 -0
- package/src/Revit/ToolRegistry.cs +109 -0
- package/src/Revit/Tools/CaptureView.cs +168 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +418 -0
- package/src/Revit/Tools/ExportDocuments.cs +256 -0
- package/src/Revit/Tools/GetElementDetails.cs +318 -0
- package/src/Revit/Tools/GetElementTypes.cs +151 -0
- package/src/Revit/Tools/GetElements.cs +725 -0
- package/src/Revit/Tools/GetModelHealth.cs +191 -0
- package/src/Revit/Tools/GetModelOverview.cs +159 -0
- package/src/Revit/Tools/ManageSelection.cs +220 -0
- package/src/Revit/Tools/SearchApiDocs.cs +610 -0
- package/src/Revit/Tools/SetParameters.cs +382 -0
- package/src/Revit/Tools/ToolSupport.cs +261 -0
- package/workspace/AGENTS.md +23 -0
- package/workspace/pi-revit-global.cmd +31 -0
- package/workspace/pi-revit-here.cmd +5 -0
- package/workspace/project-AGENTS.md +8 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
using System.IO;
|
|
2
|
+
using System.Text;
|
|
3
|
+
using System.Text.Json;
|
|
4
|
+
using System.Xml;
|
|
5
|
+
using System.Xml.Linq;
|
|
6
|
+
using Autodesk.Revit.DB;
|
|
7
|
+
|
|
8
|
+
namespace RevitBridge.Tools
|
|
9
|
+
{
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// Offline Revit API documentation search. Data source: the RevitAPI.xml and
|
|
12
|
+
/// RevitAPIUI.xml compiler doc files shipped next to the Revit install, resolved from
|
|
13
|
+
/// the loaded RevitAPI.dll location. The index is built lazily on the first query and
|
|
14
|
+
/// shared process-wide; a missing or unparsable file degrades to a warning instead of
|
|
15
|
+
/// an error. RequiresDocument = false: the tool never touches the Revit API (reading
|
|
16
|
+
/// assembly metadata for the install path is not an API call), so it runs on the
|
|
17
|
+
/// bridge's server task — index construction never blocks the Revit thread.
|
|
18
|
+
/// </summary>
|
|
19
|
+
internal sealed class SearchApiDocs : ITool
|
|
20
|
+
{
|
|
21
|
+
private const int DefaultMaxResults = 10;
|
|
22
|
+
private const int MaxResultsCap = 50;
|
|
23
|
+
private const int MaxMarkdownChars = 6_000;
|
|
24
|
+
private const int MaxSummaryChars = 600;
|
|
25
|
+
private const int MaxReturnsChars = 300;
|
|
26
|
+
private const int MaxParamDocChars = 240;
|
|
27
|
+
private const int MaxParamsPerMember = 10;
|
|
28
|
+
|
|
29
|
+
public string Name => "search_api_docs";
|
|
30
|
+
public string Label => "Search API Docs";
|
|
31
|
+
public string Description => "Search the offline Revit API documentation (the RevitAPI.xml and RevitAPIUI.xml files shipped with Revit) for types, methods, constructors, properties, fields (including enum values such as BuiltInParameter names), and events. query is a single name or substring — e.g. 'FilteredElementCollector', 'Wall.Create', 'WALL_BASE_OFFSET', 'ElementTransformUtils' — ranked: exact name first, then prefix, then substring; dotted Type.Member queries match composites. Returns signatures with summary, parameter docs, return docs, and the Revit version a member was introduced in ('since'). Works with no document open. Use it to verify exact classes, members, and signatures before writing execute_csharp code. The first query builds the index (a few seconds); later queries are instant.";
|
|
32
|
+
|
|
33
|
+
public bool RequiresDocument => false;
|
|
34
|
+
|
|
35
|
+
public object ParametersSchema => new
|
|
36
|
+
{
|
|
37
|
+
type = "object",
|
|
38
|
+
properties = new
|
|
39
|
+
{
|
|
40
|
+
query = new
|
|
41
|
+
{
|
|
42
|
+
type = "string",
|
|
43
|
+
description = "Type or member name to look up, e.g. FilteredElementCollector, Wall.Create, WALL_BASE_OFFSET. Substring and multi-word matches are ranked; Type.Member composites rank highest on exact match.",
|
|
44
|
+
},
|
|
45
|
+
kind = new
|
|
46
|
+
{
|
|
47
|
+
type = "string",
|
|
48
|
+
@enum = new[] { "type", "method", "property", "field", "event" },
|
|
49
|
+
description = "Optional filter to one member kind ('method' includes constructors; 'field' covers enum values).",
|
|
50
|
+
},
|
|
51
|
+
max_results = new
|
|
52
|
+
{
|
|
53
|
+
type = "integer",
|
|
54
|
+
description = $"Maximum matches to return (1-{MaxResultsCap}, default {DefaultMaxResults}).",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required = new[] { "query" },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
public string? PromptSnippet => "Search the offline Revit API documentation for types, members, and signatures.";
|
|
61
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
62
|
+
{
|
|
63
|
+
"Before writing execute_csharp code, verify exact classes and member signatures with search_api_docs (e.g. query 'Wall.Create' or 'FilteredElementCollector').",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
67
|
+
{
|
|
68
|
+
string query = (JsonArgs.GetString(args, "query") ?? string.Empty).Trim();
|
|
69
|
+
if (query.Length == 0)
|
|
70
|
+
throw new ArgumentException("query must be a non-empty string, e.g. 'FilteredElementCollector' or 'Wall.Create'.");
|
|
71
|
+
char? kindFilter = ParseKindFilter(JsonArgs.GetString(args, "kind"));
|
|
72
|
+
int maxResults = Math.Clamp(JsonArgs.GetInt(args, "max_results", DefaultMaxResults), 1, MaxResultsCap);
|
|
73
|
+
|
|
74
|
+
var index = Index.Value;
|
|
75
|
+
if (index.Members.Count == 0)
|
|
76
|
+
throw new InvalidOperationException("The Revit API documentation index is empty. " + string.Join(" ", index.Warnings));
|
|
77
|
+
|
|
78
|
+
var (top, total) = Search(index, query, kindFilter, maxResults);
|
|
79
|
+
|
|
80
|
+
var matches = new List<Dictionary<string, object?>>(top.Count);
|
|
81
|
+
for (int i = 0; i < top.Count; i++)
|
|
82
|
+
{
|
|
83
|
+
var member = top[i];
|
|
84
|
+
matches.Add(new Dictionary<string, object?>
|
|
85
|
+
{
|
|
86
|
+
["rank"] = i + 1,
|
|
87
|
+
["kind"] = KindLabel(member),
|
|
88
|
+
["name"] = member.FullName,
|
|
89
|
+
["signature"] = member.Signature,
|
|
90
|
+
["assembly"] = member.Assembly,
|
|
91
|
+
["since"] = member.Since,
|
|
92
|
+
["summary"] = member.Summary,
|
|
93
|
+
["parameters"] = member.Parameters?
|
|
94
|
+
.Select(pair => new Dictionary<string, object?> { ["name"] = pair.Key, ["description"] = pair.Value })
|
|
95
|
+
.ToList(),
|
|
96
|
+
["returns"] = member.Returns,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return new ToolOutput(new
|
|
101
|
+
{
|
|
102
|
+
query,
|
|
103
|
+
totalMatches = total,
|
|
104
|
+
returnedCount = matches.Count,
|
|
105
|
+
matches,
|
|
106
|
+
indexedMembers = index.Members.Count,
|
|
107
|
+
sources = index.SourceFiles,
|
|
108
|
+
warnings = index.Warnings.Count > 0 ? index.Warnings : null,
|
|
109
|
+
}, BuildMarkdown(query, top, total, index));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ----------------------------------------------------------------- search
|
|
113
|
+
|
|
114
|
+
private static readonly char[] WordSeparators = { '.', ' ', '_', '(', ')', ',', ':', '-', '/' };
|
|
115
|
+
|
|
116
|
+
private static (List<ApiMember> Top, int Total) Search(DocIndex index, string query, char? kindFilter, int maxResults)
|
|
117
|
+
{
|
|
118
|
+
string q = query.ToLowerInvariant();
|
|
119
|
+
string[] words = q.Split(WordSeparators, StringSplitOptions.RemoveEmptyEntries);
|
|
120
|
+
|
|
121
|
+
var scored = new List<(ApiMember Member, int Score)>();
|
|
122
|
+
foreach (var member in index.Members)
|
|
123
|
+
{
|
|
124
|
+
if (kindFilter is { } kind && member.Kind != kind)
|
|
125
|
+
continue;
|
|
126
|
+
int score = Score(member, q, words);
|
|
127
|
+
if (score > 0)
|
|
128
|
+
scored.Add((member, score));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var top = scored
|
|
132
|
+
.OrderByDescending(entry => entry.Score)
|
|
133
|
+
.ThenBy(entry => entry.Member.Composite.Length)
|
|
134
|
+
.ThenBy(entry => entry.Member.FullName, StringComparer.Ordinal)
|
|
135
|
+
.Take(maxResults)
|
|
136
|
+
.Select(entry => entry.Member)
|
|
137
|
+
.ToList();
|
|
138
|
+
return (top, scored.Count);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private static int Score(ApiMember member, string q, string[] words)
|
|
142
|
+
{
|
|
143
|
+
int score;
|
|
144
|
+
if (member.CompositeLower == q) score = 1000;
|
|
145
|
+
else if (member.ShortNameLower == q) score = 900;
|
|
146
|
+
else if (member.CompositeLower.StartsWith(q, StringComparison.Ordinal)) score = 700;
|
|
147
|
+
else if (member.ShortNameLower.StartsWith(q, StringComparison.Ordinal)) score = 650;
|
|
148
|
+
else if (member.CompositeLower.Contains(q, StringComparison.Ordinal)) score = 500;
|
|
149
|
+
else if (member.FullNameLower.Contains(q, StringComparison.Ordinal)) score = 400;
|
|
150
|
+
else if (words.Length > 1 && words.All(word => member.FullNameLower.Contains(word, StringComparison.Ordinal))) score = 300;
|
|
151
|
+
else return 0;
|
|
152
|
+
|
|
153
|
+
if (member.Kind == 'T') score += 30;
|
|
154
|
+
if (member.FullNameLower.StartsWith("autodesk.revit.db.", StringComparison.Ordinal)) score += 10;
|
|
155
|
+
return score;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private static string BuildMarkdown(string query, IReadOnlyList<ApiMember> top, int total, DocIndex index)
|
|
159
|
+
{
|
|
160
|
+
var markdown = new StringBuilder();
|
|
161
|
+
markdown.Append($"Revit API docs for '{query}': ");
|
|
162
|
+
if (total == 0)
|
|
163
|
+
{
|
|
164
|
+
markdown.Append("no matches. Try a shorter substring (a class name like 'FilteredElementCollector'), a Type.Member composite like 'Wall.Create', or drop the kind filter.");
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
{
|
|
168
|
+
markdown.Append(total == top.Count ? $"{total} match(es)." : $"top {top.Count} of {total} matches.");
|
|
169
|
+
for (int i = 0; i < top.Count; i++)
|
|
170
|
+
{
|
|
171
|
+
var member = top[i];
|
|
172
|
+
string origin = member.Since is null ? member.Assembly : $"{member.Assembly}, since {member.Since}";
|
|
173
|
+
string line = $"\n{i + 1}. **{member.Signature}** — {KindLabel(member)} ({origin}) — {FirstSentence(member.Summary) ?? "(no summary)"}";
|
|
174
|
+
if (markdown.Length + line.Length > MaxMarkdownChars)
|
|
175
|
+
{
|
|
176
|
+
markdown.Append($"\n… capped; {top.Count - i} more match(es) in details.payload.matches.");
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
markdown.Append(line);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
foreach (string warning in index.Warnings)
|
|
183
|
+
markdown.Append($"\nNote: {warning}");
|
|
184
|
+
return markdown.ToString();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static string KindLabel(ApiMember member) => member.IsConstructor ? "constructor" : member.Kind switch
|
|
188
|
+
{
|
|
189
|
+
'T' => "type",
|
|
190
|
+
'M' => "method",
|
|
191
|
+
'P' => "property",
|
|
192
|
+
'F' => "field",
|
|
193
|
+
'E' => "event",
|
|
194
|
+
_ => "member",
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
private static char? ParseKindFilter(string? kind)
|
|
198
|
+
{
|
|
199
|
+
if (string.IsNullOrWhiteSpace(kind))
|
|
200
|
+
return null;
|
|
201
|
+
return kind.Trim().ToLowerInvariant() switch
|
|
202
|
+
{
|
|
203
|
+
"type" or "class" or "t" => 'T',
|
|
204
|
+
"method" or "constructor" or "m" => 'M',
|
|
205
|
+
"property" or "p" => 'P',
|
|
206
|
+
"field" or "enum" or "f" => 'F',
|
|
207
|
+
"event" or "e" => 'E',
|
|
208
|
+
_ => throw new ArgumentException($"Unknown kind: {kind}. Use type, method, property, field, or event."),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private static string? FirstSentence(string? text)
|
|
213
|
+
{
|
|
214
|
+
if (string.IsNullOrEmpty(text))
|
|
215
|
+
return null;
|
|
216
|
+
int end = text.IndexOf(". ", StringComparison.Ordinal);
|
|
217
|
+
string sentence = end >= 0 ? text[..(end + 1)] : text;
|
|
218
|
+
return sentence.Length <= 220 ? sentence : sentence[..220] + "…";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ------------------------------------------------------------------ index
|
|
222
|
+
|
|
223
|
+
private sealed class ApiMember
|
|
224
|
+
{
|
|
225
|
+
public required char Kind { get; init; }
|
|
226
|
+
public required bool IsConstructor { get; init; }
|
|
227
|
+
public required string Assembly { get; init; }
|
|
228
|
+
|
|
229
|
+
/// <summary>Namespace-qualified name without the kind prefix or parameter list.</summary>
|
|
230
|
+
public required string FullName { get; init; }
|
|
231
|
+
|
|
232
|
+
/// <summary>"Wall" for types, "Wall.Create" for members — the primary match target.</summary>
|
|
233
|
+
public required string Composite { get; init; }
|
|
234
|
+
|
|
235
|
+
public required string Signature { get; init; }
|
|
236
|
+
public required string FullNameLower { get; init; }
|
|
237
|
+
public required string CompositeLower { get; init; }
|
|
238
|
+
public required string ShortNameLower { get; init; }
|
|
239
|
+
public string? Summary { get; init; }
|
|
240
|
+
public string? Returns { get; init; }
|
|
241
|
+
public string? Since { get; init; }
|
|
242
|
+
public IReadOnlyList<KeyValuePair<string, string>>? Parameters { get; init; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private sealed class DocIndex
|
|
246
|
+
{
|
|
247
|
+
public required IReadOnlyList<ApiMember> Members { get; init; }
|
|
248
|
+
public required IReadOnlyList<string> SourceFiles { get; init; }
|
|
249
|
+
public required IReadOnlyList<string> Warnings { get; init; }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// <summary>Built once per Revit session on first query; thread-safe via Lazy. The
|
|
253
|
+
/// factory never throws — file problems are captured as warnings instead.</summary>
|
|
254
|
+
private static readonly Lazy<DocIndex> Index = new(BuildIndex, LazyThreadSafetyMode.ExecutionAndPublication);
|
|
255
|
+
|
|
256
|
+
private static DocIndex BuildIndex()
|
|
257
|
+
{
|
|
258
|
+
var members = new List<ApiMember>(90_000);
|
|
259
|
+
var sources = new List<string>();
|
|
260
|
+
var warnings = new List<string>();
|
|
261
|
+
|
|
262
|
+
foreach (string path in CandidateXmlFiles())
|
|
263
|
+
{
|
|
264
|
+
if (!File.Exists(path))
|
|
265
|
+
{
|
|
266
|
+
warnings.Add($"Documentation file not found: {path}.");
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
try
|
|
270
|
+
{
|
|
271
|
+
ParseFile(path, members);
|
|
272
|
+
sources.Add(path);
|
|
273
|
+
}
|
|
274
|
+
catch (Exception ex)
|
|
275
|
+
{
|
|
276
|
+
warnings.Add($"Failed to parse {path}: {ex.Message}");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
AddBuiltInCategoryValues(members, warnings);
|
|
281
|
+
|
|
282
|
+
return new DocIndex { Members = members, SourceFiles = sources, Warnings = warnings };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// <summary>Autodesk's XML documents only a handful of the ~1000 BuiltInCategory
|
|
286
|
+
/// values. Synthesize the rest from the loaded RevitAPI.dll via reflection —
|
|
287
|
+
/// pure metadata, safe off the Revit thread.</summary>
|
|
288
|
+
private static void AddBuiltInCategoryValues(List<ApiMember> members, List<string> warnings)
|
|
289
|
+
{
|
|
290
|
+
try
|
|
291
|
+
{
|
|
292
|
+
var existing = new HashSet<string>(
|
|
293
|
+
members
|
|
294
|
+
.Where(member => member.FullName.StartsWith("Autodesk.Revit.DB.BuiltInCategory.", StringComparison.Ordinal))
|
|
295
|
+
.Select(member => member.FullName),
|
|
296
|
+
StringComparer.Ordinal);
|
|
297
|
+
|
|
298
|
+
foreach (string name in Enum.GetNames(typeof(Autodesk.Revit.DB.BuiltInCategory)))
|
|
299
|
+
{
|
|
300
|
+
string fullName = "Autodesk.Revit.DB.BuiltInCategory." + name;
|
|
301
|
+
if (!existing.Add(fullName))
|
|
302
|
+
continue;
|
|
303
|
+
|
|
304
|
+
string composite = "BuiltInCategory." + name;
|
|
305
|
+
members.Add(new ApiMember
|
|
306
|
+
{
|
|
307
|
+
Kind = 'F',
|
|
308
|
+
IsConstructor = false,
|
|
309
|
+
Assembly = "RevitAPI",
|
|
310
|
+
FullName = fullName,
|
|
311
|
+
Composite = composite,
|
|
312
|
+
Signature = composite,
|
|
313
|
+
FullNameLower = fullName.ToLowerInvariant(),
|
|
314
|
+
CompositeLower = composite.ToLowerInvariant(),
|
|
315
|
+
ShortNameLower = name.ToLowerInvariant(),
|
|
316
|
+
Summary = "BuiltInCategory enum value — usable as the 'category' argument of get_elements/get_element_types and with FilteredElementCollector.OfCategory in execute_csharp.",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (Exception ex)
|
|
321
|
+
{
|
|
322
|
+
warnings.Add($"Could not enumerate BuiltInCategory values: {ex.Message}");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// <summary>RevitAPI.xml + RevitAPIUI.xml next to the loaded RevitAPI.dll, with the
|
|
327
|
+
/// standard install directory as the fallback.</summary>
|
|
328
|
+
private static IEnumerable<string> CandidateXmlFiles()
|
|
329
|
+
{
|
|
330
|
+
string? installDir = null;
|
|
331
|
+
try
|
|
332
|
+
{
|
|
333
|
+
string location = typeof(Document).Assembly.Location;
|
|
334
|
+
if (!string.IsNullOrEmpty(location))
|
|
335
|
+
installDir = Path.GetDirectoryName(location);
|
|
336
|
+
}
|
|
337
|
+
catch
|
|
338
|
+
{
|
|
339
|
+
// Fall through to the standard path.
|
|
340
|
+
}
|
|
341
|
+
installDir = string.IsNullOrEmpty(installDir) ? @"C:\Program Files\Autodesk\Revit 2025" : installDir;
|
|
342
|
+
yield return Path.Combine(installDir, "RevitAPI.xml");
|
|
343
|
+
yield return Path.Combine(installDir, "RevitAPIUI.xml");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private static void ParseFile(string path, List<ApiMember> members)
|
|
347
|
+
{
|
|
348
|
+
string assemblyName = Path.GetFileNameWithoutExtension(path);
|
|
349
|
+
using var reader = XmlReader.Create(path, new XmlReaderSettings { IgnoreComments = true });
|
|
350
|
+
reader.MoveToContent();
|
|
351
|
+
while (!reader.EOF)
|
|
352
|
+
{
|
|
353
|
+
if (reader.NodeType == XmlNodeType.Element && reader.Name == "member")
|
|
354
|
+
{
|
|
355
|
+
string? rawName = reader.GetAttribute("name");
|
|
356
|
+
// XNode.ReadFrom consumes the whole <member> element and leaves the
|
|
357
|
+
// reader on the following node — do not Read() again here.
|
|
358
|
+
var element = (XElement)XNode.ReadFrom(reader);
|
|
359
|
+
if (rawName != null && CreateMember(rawName, element, assemblyName) is { } member)
|
|
360
|
+
members.Add(member);
|
|
361
|
+
}
|
|
362
|
+
else
|
|
363
|
+
{
|
|
364
|
+
reader.Read();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private static ApiMember? CreateMember(string rawName, XElement element, string assemblyName)
|
|
370
|
+
{
|
|
371
|
+
if (rawName.Length < 3 || rawName[1] != ':')
|
|
372
|
+
return null;
|
|
373
|
+
char kind = rawName[0];
|
|
374
|
+
if (kind is not ('T' or 'M' or 'P' or 'F' or 'E'))
|
|
375
|
+
return null;
|
|
376
|
+
|
|
377
|
+
string body = rawName[2..];
|
|
378
|
+
int paren = body.IndexOf('(');
|
|
379
|
+
string path = paren >= 0 ? body[..paren] : body;
|
|
380
|
+
string? paramText = null;
|
|
381
|
+
if (paren >= 0)
|
|
382
|
+
{
|
|
383
|
+
int close = body.LastIndexOf(')');
|
|
384
|
+
if (close > paren)
|
|
385
|
+
paramText = body[(paren + 1)..close];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
string[] segments = path.Split('.');
|
|
389
|
+
string shortName;
|
|
390
|
+
string composite;
|
|
391
|
+
string signature;
|
|
392
|
+
bool isConstructor = false;
|
|
393
|
+
|
|
394
|
+
if (kind == 'T')
|
|
395
|
+
{
|
|
396
|
+
shortName = StripArity(segments[^1]);
|
|
397
|
+
composite = shortName;
|
|
398
|
+
signature = shortName;
|
|
399
|
+
}
|
|
400
|
+
else
|
|
401
|
+
{
|
|
402
|
+
string memberName = segments[^1];
|
|
403
|
+
string typeName = segments.Length >= 2 ? StripArity(segments[^2]) : string.Empty;
|
|
404
|
+
if (memberName is "#ctor" or "#cctor")
|
|
405
|
+
{
|
|
406
|
+
isConstructor = true;
|
|
407
|
+
shortName = typeName;
|
|
408
|
+
composite = typeName.Length > 0 ? $"{typeName}.{typeName}" : typeName;
|
|
409
|
+
signature = $"new {typeName}({(paramText is null ? string.Empty : ShortenTypeText(paramText))})";
|
|
410
|
+
}
|
|
411
|
+
else
|
|
412
|
+
{
|
|
413
|
+
// Explicit interface implementations look like System#IDisposable#Dispose.
|
|
414
|
+
int hash = memberName.LastIndexOf('#');
|
|
415
|
+
if (hash >= 0)
|
|
416
|
+
memberName = memberName[(hash + 1)..];
|
|
417
|
+
shortName = StripArity(memberName);
|
|
418
|
+
composite = typeName.Length > 0 ? $"{typeName}.{shortName}" : shortName;
|
|
419
|
+
signature = kind == 'M' || paramText != null
|
|
420
|
+
? $"{composite}({(paramText is null ? string.Empty : ShortenTypeText(paramText))})"
|
|
421
|
+
: composite;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
List<KeyValuePair<string, string>>? parameters = null;
|
|
426
|
+
foreach (var param in element.Elements("param"))
|
|
427
|
+
{
|
|
428
|
+
if (parameters is { Count: >= MaxParamsPerMember })
|
|
429
|
+
break;
|
|
430
|
+
string? name = param.Attribute("name")?.Value;
|
|
431
|
+
string? text = Cap(CleanDocText(param), MaxParamDocChars);
|
|
432
|
+
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(text))
|
|
433
|
+
continue;
|
|
434
|
+
(parameters ??= new List<KeyValuePair<string, string>>()).Add(new KeyValuePair<string, string>(name, text));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return new ApiMember
|
|
438
|
+
{
|
|
439
|
+
Kind = kind,
|
|
440
|
+
IsConstructor = isConstructor,
|
|
441
|
+
Assembly = assemblyName,
|
|
442
|
+
FullName = path,
|
|
443
|
+
Composite = composite,
|
|
444
|
+
Signature = signature,
|
|
445
|
+
FullNameLower = path.ToLowerInvariant(),
|
|
446
|
+
CompositeLower = composite.ToLowerInvariant(),
|
|
447
|
+
ShortNameLower = shortName.ToLowerInvariant(),
|
|
448
|
+
Summary = Cap(CleanDocText(element.Element("summary")), MaxSummaryChars),
|
|
449
|
+
Returns = Cap(CleanDocText(element.Element("returns")), MaxReturnsChars),
|
|
450
|
+
Since = CleanDocText(element.Element("since")),
|
|
451
|
+
Parameters = parameters,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ------------------------------------------------------------- text utils
|
|
456
|
+
|
|
457
|
+
/// <summary>Flattens doc XML to plain text: see/seealso cref -> short type name,
|
|
458
|
+
/// paramref/typeparamref -> the name, all other tags -> their inner text;
|
|
459
|
+
/// whitespace collapsed.</summary>
|
|
460
|
+
private static string? CleanDocText(XElement? element)
|
|
461
|
+
{
|
|
462
|
+
if (element is null)
|
|
463
|
+
return null;
|
|
464
|
+
var sb = new StringBuilder();
|
|
465
|
+
foreach (var node in element.Nodes())
|
|
466
|
+
AppendNode(sb, node);
|
|
467
|
+
string text = CollapseWhitespace(sb.ToString());
|
|
468
|
+
return text.Length == 0 ? null : text;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private static void AppendNode(StringBuilder sb, XNode node)
|
|
472
|
+
{
|
|
473
|
+
switch (node)
|
|
474
|
+
{
|
|
475
|
+
case XText text:
|
|
476
|
+
sb.Append(text.Value);
|
|
477
|
+
break;
|
|
478
|
+
case XElement element:
|
|
479
|
+
switch (element.Name.LocalName)
|
|
480
|
+
{
|
|
481
|
+
case "see" or "seealso":
|
|
482
|
+
string? target = element.Attribute("cref")?.Value ?? element.Attribute("langword")?.Value;
|
|
483
|
+
if (target != null)
|
|
484
|
+
sb.Append(ShortCref(target));
|
|
485
|
+
else
|
|
486
|
+
foreach (var child in element.Nodes())
|
|
487
|
+
AppendNode(sb, child);
|
|
488
|
+
break;
|
|
489
|
+
case "paramref" or "typeparamref":
|
|
490
|
+
sb.Append(element.Attribute("name")?.Value);
|
|
491
|
+
break;
|
|
492
|
+
default:
|
|
493
|
+
foreach (var child in element.Nodes())
|
|
494
|
+
AppendNode(sb, child);
|
|
495
|
+
sb.Append(' ');
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// <summary>"T:Autodesk.Revit.DB.Wall" -> "Wall"; "M:...Wall.Create(...)" -> "Create".</summary>
|
|
503
|
+
private static string ShortCref(string cref)
|
|
504
|
+
{
|
|
505
|
+
string text = cref.Length > 2 && cref[1] == ':' ? cref[2..] : cref;
|
|
506
|
+
int paren = text.IndexOf('(');
|
|
507
|
+
if (paren >= 0)
|
|
508
|
+
text = text[..paren];
|
|
509
|
+
int dot = text.LastIndexOf('.');
|
|
510
|
+
if (dot >= 0)
|
|
511
|
+
text = text[(dot + 1)..];
|
|
512
|
+
return StripArity(text);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// <summary>Shortens a doc-id parameter list: namespaces stripped, {} -> <>,
|
|
516
|
+
/// `N arity dropped, ``N generic parameter references -> T, @ (by-ref) -> &,
|
|
517
|
+
/// System primitives -> C# keywords. "Autodesk.Revit.DB.Document,System.Collections.Generic.IList{Autodesk.Revit.DB.Curve},System.Boolean"
|
|
518
|
+
/// -> "Document, IList<Curve>, bool".</summary>
|
|
519
|
+
private static string ShortenTypeText(string text)
|
|
520
|
+
{
|
|
521
|
+
var result = new StringBuilder(text.Length);
|
|
522
|
+
int i = 0;
|
|
523
|
+
while (i < text.Length)
|
|
524
|
+
{
|
|
525
|
+
char c = text[i];
|
|
526
|
+
if (char.IsLetter(c) || c == '_')
|
|
527
|
+
{
|
|
528
|
+
int start = i;
|
|
529
|
+
int lastDot = -1;
|
|
530
|
+
while (i < text.Length && (char.IsLetterOrDigit(text[i]) || text[i] is '_' or '.'))
|
|
531
|
+
{
|
|
532
|
+
if (text[i] == '.')
|
|
533
|
+
lastDot = i;
|
|
534
|
+
i++;
|
|
535
|
+
}
|
|
536
|
+
result.Append(MapTypeKeyword(text[(lastDot >= 0 ? lastDot + 1 : start)..i]));
|
|
537
|
+
}
|
|
538
|
+
else if (c == '`')
|
|
539
|
+
{
|
|
540
|
+
// Directly after an identifier: a generic arity suffix (List`1) — drop it.
|
|
541
|
+
// Standalone: a generic parameter reference (``0) — render as T.
|
|
542
|
+
bool arity = i > 0 && char.IsLetterOrDigit(text[i - 1]);
|
|
543
|
+
while (i < text.Length && (text[i] == '`' || char.IsDigit(text[i])))
|
|
544
|
+
i++;
|
|
545
|
+
if (!arity)
|
|
546
|
+
result.Append('T');
|
|
547
|
+
}
|
|
548
|
+
else
|
|
549
|
+
{
|
|
550
|
+
result.Append(c switch { '{' => '<', '}' => '>', '@' => '&', _ => c });
|
|
551
|
+
if (c == ',')
|
|
552
|
+
result.Append(' ');
|
|
553
|
+
i++;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return result.ToString();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private static string MapTypeKeyword(string name) => name switch
|
|
560
|
+
{
|
|
561
|
+
"String" => "string",
|
|
562
|
+
"Boolean" => "bool",
|
|
563
|
+
"Int32" => "int",
|
|
564
|
+
"Int64" => "long",
|
|
565
|
+
"Int16" => "short",
|
|
566
|
+
"Double" => "double",
|
|
567
|
+
"Single" => "float",
|
|
568
|
+
"Object" => "object",
|
|
569
|
+
"Void" => "void",
|
|
570
|
+
"Byte" => "byte",
|
|
571
|
+
"SByte" => "sbyte",
|
|
572
|
+
"Char" => "char",
|
|
573
|
+
"Decimal" => "decimal",
|
|
574
|
+
"UInt16" => "ushort",
|
|
575
|
+
"UInt32" => "uint",
|
|
576
|
+
"UInt64" => "ulong",
|
|
577
|
+
_ => name,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
private static string StripArity(string name)
|
|
581
|
+
{
|
|
582
|
+
int tick = name.IndexOf('`');
|
|
583
|
+
return tick >= 0 ? name[..tick] : name;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private static string CollapseWhitespace(string text)
|
|
587
|
+
{
|
|
588
|
+
var sb = new StringBuilder(text.Length);
|
|
589
|
+
bool pendingSpace = false;
|
|
590
|
+
foreach (char c in text)
|
|
591
|
+
{
|
|
592
|
+
if (char.IsWhiteSpace(c))
|
|
593
|
+
{
|
|
594
|
+
pendingSpace = sb.Length > 0;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (pendingSpace)
|
|
598
|
+
{
|
|
599
|
+
sb.Append(' ');
|
|
600
|
+
pendingSpace = false;
|
|
601
|
+
}
|
|
602
|
+
sb.Append(c);
|
|
603
|
+
}
|
|
604
|
+
return sb.ToString();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private static string? Cap(string? text, int max)
|
|
608
|
+
=> text is null || text.Length <= max ? text : text[..max] + "…";
|
|
609
|
+
}
|
|
610
|
+
}
|