Skip to content

Commit 86cc141

Browse files
committed
- Added support for additional completion items in the proto3 language.
- Improved error handling in document generation and directory creation functions. - Updated the `.gitignore` to include `.direnv/`. - Refactored `formatFile` to handle errors more gracefully. - Enhanced `rightClickGenDoc` to catch and log errors during execution. (cherry picked from commit 9ee2714)
1 parent f7a3f84 commit 86cc141

9 files changed

Lines changed: 400 additions & 139 deletions

File tree

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake github:acehinnnqru/devshells#nodejs-22

src/api/completion/completion.ts

Lines changed: 245 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,250 @@
1+
import * as vscode from 'vscode';
12

2-
import vscode = require('vscode');
3+
const SCALAR_TYPES = [
4+
'double',
5+
'float',
6+
'int32',
7+
'int64',
8+
'uint32',
9+
'uint64',
10+
'sint32',
11+
'sint64',
12+
'fixed32',
13+
'fixed64',
14+
'sfixed32',
15+
'sfixed64',
16+
'bool',
17+
'string',
18+
'bytes',
19+
];
320

4-
class Proto3CompletionItemProvider implements vscode.CompletionItemProvider {
5-
provideCompletionItems(
6-
document: vscode.TextDocument,
7-
position: vscode.Position,
8-
token: vscode.CancellationToken,
9-
context: vscode.CompletionContext
10-
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList<vscode.CompletionItem>> {
11-
throw new Error('Method not implemented.');
12-
}
13-
resolveCompletionItem?(item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CompletionItem> {
14-
throw new Error('Method not implemented.');
15-
}
21+
const FILE_KEYWORDS = [
22+
'syntax',
23+
'package',
24+
'import',
25+
'option',
26+
'message',
27+
'enum',
28+
'service',
29+
'extend',
30+
];
31+
32+
const FIELD_KEYWORDS = [
33+
'repeated',
34+
'optional',
35+
'map',
36+
'oneof',
37+
'reserved',
38+
'message',
39+
'enum',
40+
];
41+
42+
const SERVICE_KEYWORDS = ['rpc', 'option', 'stream', 'returns'];
43+
44+
const WELL_KNOWN_TYPES = [
45+
'google.protobuf.Any',
46+
'google.protobuf.Timestamp',
47+
'google.protobuf.Duration',
48+
'google.protobuf.Empty',
49+
'google.protobuf.Struct',
50+
'google.protobuf.Value',
51+
'google.protobuf.ListValue',
52+
'google.protobuf.FieldMask',
53+
'google.protobuf.BoolValue',
54+
'google.protobuf.BytesValue',
55+
'google.protobuf.DoubleValue',
56+
'google.protobuf.FloatValue',
57+
'google.protobuf.Int32Value',
58+
'google.protobuf.Int64Value',
59+
'google.protobuf.StringValue',
60+
'google.protobuf.UInt32Value',
61+
'google.protobuf.UInt64Value',
62+
];
63+
64+
type BlockKind = 'message' | 'enum' | 'service' | 'oneof';
65+
66+
function linePrefix(document: vscode.TextDocument, position: vscode.Position): string {
67+
return document.lineAt(position).text.substring(0, position.character);
68+
}
69+
70+
function stripLineComment(line: string): string {
71+
return line.split('//')[0];
72+
}
73+
74+
function advanceBlockScan(line: string, stack: BlockKind[]): void {
75+
const cleaned = stripLineComment(line);
76+
let idx = 0;
77+
while (idx < cleaned.length) {
78+
const rest = cleaned.slice(idx);
79+
const open = rest.match(/^\b(message|enum|service|oneof)\s+\w+\s*\{/);
80+
if (open) {
81+
stack.push(open[1] as BlockKind);
82+
idx += open[0].length;
83+
continue;
84+
}
85+
if (rest[0] === '}') {
86+
stack.pop();
87+
idx += 1;
88+
continue;
89+
}
90+
idx += 1;
91+
}
1692
}
1793

94+
function getScopeStack(document: vscode.TextDocument, position: vscode.Position): BlockKind[] {
95+
const stack: BlockKind[] = [];
96+
for (let line = 0; line <= position.line; line++) {
97+
let text = document.lineAt(line).text;
98+
if (line === position.line) {
99+
text = text.substring(0, position.character);
100+
}
101+
advanceBlockScan(text, stack);
102+
}
103+
return stack;
104+
}
105+
106+
function getDeclaredTypes(text: string): { messages: string[]; enums: string[] } {
107+
const messages: string[] = [];
108+
const enums: string[] = [];
109+
for (const m of text.matchAll(/\bmessage\s+(\w+)\s*\{/g)) {
110+
messages.push(m[1]);
111+
}
112+
for (const m of text.matchAll(/\benum\s+(\w+)\s*\{/g)) {
113+
enums.push(m[1]);
114+
}
115+
return {
116+
messages: [...new Set(messages)],
117+
enums: [...new Set(enums)],
118+
};
119+
}
120+
121+
function kw(label: string, detail?: string): vscode.CompletionItem {
122+
const i = new vscode.CompletionItem(label, vscode.CompletionItemKind.Keyword);
123+
if (detail) {
124+
i.detail = detail;
125+
}
126+
return i;
127+
}
128+
129+
function scalar(label: string): vscode.CompletionItem {
130+
return new vscode.CompletionItem(label, vscode.CompletionItemKind.TypeParameter);
131+
}
132+
133+
function wellKnown(label: string): vscode.CompletionItem {
134+
const i = new vscode.CompletionItem(label, vscode.CompletionItemKind.Interface);
135+
i.detail = 'Well-known type';
136+
return i;
137+
}
18138

19-
export { Proto3CompletionItemProvider };
139+
function addUserTypes(
140+
items: vscode.CompletionItem[],
141+
messages: string[],
142+
enums: string[]
143+
): void {
144+
for (const name of messages) {
145+
const i = new vscode.CompletionItem(name, vscode.CompletionItemKind.Class);
146+
i.detail = 'message';
147+
items.push(i);
148+
}
149+
for (const name of enums) {
150+
const i = new vscode.CompletionItem(name, vscode.CompletionItemKind.Enum);
151+
i.detail = 'enum';
152+
items.push(i);
153+
}
154+
}
155+
156+
function addRpcTypeItems(
157+
items: vscode.CompletionItem[],
158+
messages: string[],
159+
enums: string[]
160+
): void {
161+
for (const t of WELL_KNOWN_TYPES) {
162+
items.push(wellKnown(t));
163+
}
164+
addUserTypes(items, messages, enums);
165+
}
166+
167+
function syntaxProto3Completion(): vscode.CompletionItem[] {
168+
const i = new vscode.CompletionItem('proto3', vscode.CompletionItemKind.EnumMember);
169+
i.detail = 'syntax = "proto3"';
170+
return [i];
171+
}
172+
173+
function isInsideUnclosedString(prefix: string): boolean {
174+
return ((prefix.match(/"/g) || []).length % 2) === 1;
175+
}
176+
177+
export class Proto3CompletionItemProvider implements vscode.CompletionItemProvider {
178+
provideCompletionItems(
179+
document: vscode.TextDocument,
180+
position: vscode.Position,
181+
token: vscode.CancellationToken
182+
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList<vscode.CompletionItem>> {
183+
if (token.isCancellationRequested) {
184+
return [];
185+
}
186+
187+
const prefix = linePrefix(document, position);
188+
if (isInsideUnclosedString(prefix)) {
189+
if (/\bsyntax\s*=\s*"(\w*)$/.test(prefix)) {
190+
return syntaxProto3Completion();
191+
}
192+
return [];
193+
}
194+
195+
const fullText = document.getText();
196+
const { messages, enums } = getDeclaredTypes(fullText);
197+
198+
if (/\brpc\s+\w+\s*\(\s*([\w.]*)$/.test(prefix)) {
199+
const items: vscode.CompletionItem[] = [];
200+
addRpcTypeItems(items, messages, enums);
201+
return items;
202+
}
203+
if (/\breturns\s*\(\s*([\w.]*)$/.test(prefix)) {
204+
const items: vscode.CompletionItem[] = [];
205+
addRpcTypeItems(items, messages, enums);
206+
return items;
207+
}
208+
209+
const stack = getScopeStack(document, position);
210+
const top = stack.length > 0 ? stack[stack.length - 1] : undefined;
211+
212+
if (top === 'enum') {
213+
return [kw('reserved'), kw('option')];
214+
}
215+
216+
if (top === 'service') {
217+
const items: vscode.CompletionItem[] = [];
218+
for (const w of SERVICE_KEYWORDS) {
219+
items.push(kw(w));
220+
}
221+
return items;
222+
}
223+
224+
if (top === 'message' || top === 'oneof') {
225+
const items: vscode.CompletionItem[] = [];
226+
for (const w of FIELD_KEYWORDS) {
227+
items.push(kw(w));
228+
}
229+
for (const s of SCALAR_TYPES) {
230+
items.push(scalar(s));
231+
}
232+
for (const t of WELL_KNOWN_TYPES) {
233+
items.push(wellKnown(t));
234+
}
235+
addUserTypes(items, messages, enums);
236+
return items;
237+
}
238+
239+
// File scope (or unknown nested): top-level declarations + types for imports / forward refs
240+
const items: vscode.CompletionItem[] = [];
241+
for (const w of FILE_KEYWORDS) {
242+
items.push(kw(w));
243+
}
244+
for (const t of WELL_KNOWN_TYPES) {
245+
items.push(wellKnown(t));
246+
}
247+
addUserTypes(items, messages, enums);
248+
return items;
249+
}
250+
}

src/extension.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,18 @@ import {formatFile, isClangFormat} from "./repo/format/format";
1414
// your extension is activated the very first time the command is executed
1515
export function activate(context: vscode.ExtensionContext) {
1616
// 注册一个自动补全
17-
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(Proto3, new Proto3CompletionItemProvider(), '.', '\"'));
17+
context.subscriptions.push(
18+
vscode.languages.registerCompletionItemProvider(Proto3, new Proto3CompletionItemProvider(), '.', '\"', '(')
19+
);
20+
21+
function provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Definition | vscode.LocationLink[]> {
22+
let word = document.getText(document.getWordRangeAtPosition(position));
23+
console.log(word);
24+
console.log(position.line);
25+
26+
let path = document.uri.path;
27+
return new vscode.Location(vscode.Uri.file(path), new vscode.Position(3, 10));
28+
}
1829

1930
vscode.languages.registerDocumentFormattingEditProvider('proto3', {
2031
provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] {
@@ -30,8 +41,9 @@ export function activate(context: vscode.ExtensionContext) {
3041
vscode.commands.registerTextEditorCommand('proto3.menus_gendoc', (editor) => {
3142
void rightClickGenDoc(editor).catch((e) => console.error('proto3.menus_gendoc', e));
3243
}),
33-
vscode.languages.registerDefinitionProvider(['proto3'], createProto3DefinitionProvider())
34-
);
44+
vscode.languages.registerDefinitionProvider(['proto3'], {
45+
provideDefinition
46+
}));
3547
}
3648

3749

src/repo/doc/doc.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import path = require('path');
22
import cp = require('child_process');
3+
import { promisify } from 'util';
34
import vscode = require('vscode');
45
import { readActiveEditor } from '../../utils/active_editor';
56
import { execAreadyInstall, getExecPath, protoDoc, showInstallNotify, ToolInfo, toolsMap } from '../../utils/tools';
67
import { createDir } from '../../utils/dir';
7-
import { showGenDocSucNotify } from '../../utils/notify';
8+
import { showGenDocSucNotify, showErrorNotify } from '../../utils/notify';
9+
10+
const execFileAsync = promisify(cp.execFile);
811

912
// 操作时校验是否已安装工具
10-
export function generateMarkdown(ctx: vscode.ExtensionContext) {
13+
export function generateMarkdown(_ctx: vscode.ExtensionContext) {
1114
const tool = getTool(protoDoc);
1215
if (!tool) {
1316
return;
@@ -16,24 +19,24 @@ export function generateMarkdown(ctx: vscode.ExtensionContext) {
1619
if (!editor) {
1720
return;
1821
}
19-
editorToMarkDown(editor, tool);
22+
void runEditorToMarkdown(editor, tool);
2023
}
2124

2225

23-
export async function rightClickGenDoc(editor: vscode.TextEditor) {
26+
export async function rightClickGenDoc(editor: vscode.TextEditor | undefined) {
2427
if (!editor) {
2528
vscode.window.showWarningMessage("Failed to get live window!");
29+
return;
2630
}
2731
const tool = getTool(protoDoc);
2832
if (!tool) {
2933
return;
3034
}
31-
editorToMarkDown(editor, tool);
35+
await runEditorToMarkdown(editor, tool);
3236
}
3337

3438

35-
function editorToMarkDown(editor: vscode.TextEditor, tool: ToolInfo) {
36-
// 文件路径
39+
async function runEditorToMarkdown(editor: vscode.TextEditor, tool: ToolInfo) {
3740
const fileName = editor.document.fileName;
3841
const workDir = path.dirname(fileName);
3942
const execPath = getExecPath(tool);
@@ -42,17 +45,32 @@ function editorToMarkDown(editor: vscode.TextEditor, tool: ToolInfo) {
4245
const outPath = path.join(workDir, String(docPath));
4346
const language = config.get("template_language");
4447

45-
createDir(outPath);
46-
cp.execFile(execPath, ["doc",
47-
"--proto", fileName,
48-
"--out", outPath,
49-
"--language", String(language)], (error, stdout, stderr) => {
50-
if (error) {
51-
throw error;
52-
}
53-
console.log(stdout);
54-
});
55-
showGenDocSucNotify(outPath);
48+
try {
49+
await createDir(outPath);
50+
} catch {
51+
return;
52+
}
53+
54+
try {
55+
const { stdout, stderr } = await execFileAsync(execPath, [
56+
"doc",
57+
"--proto", fileName,
58+
"--out", outPath,
59+
"--language", String(language),
60+
]);
61+
if (stdout?.length) {
62+
console.log(stdout.toString());
63+
}
64+
if (stderr?.length) {
65+
console.warn(stderr.toString());
66+
}
67+
showGenDocSucNotify(outPath);
68+
} catch (error: unknown) {
69+
const err = error as NodeJS.ErrnoException & { stderr?: Buffer };
70+
const detail =
71+
(err.stderr && err.stderr.toString().trim()) || err.message || String(error);
72+
showErrorNotify(new Error(detail) as NodeJS.ErrnoException);
73+
}
5674
}
5775

5876
function getTool(toolName: string): ToolInfo | undefined {

0 commit comments

Comments
 (0)