mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2026-01-01 17:48:36 +03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86ae182088 | ||
|
|
658270e742 | ||
|
|
170d3d4819 | ||
|
|
1a84a57647 | ||
|
|
9b16946ba9 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -40,7 +40,7 @@ body:
|
||||
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: "2.2.0"
|
||||
placeholder: "2.3.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -18,7 +18,7 @@ body:
|
||||
description: >
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: 4.2.2.stable, 4.3.rc (88d932506)
|
||||
placeholder: 4.3.stable, 4.4.dev1 (28a72fa43)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -29,7 +29,7 @@ body:
|
||||
Use the **Help > About** menu to see your current version.
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: "1.91.1"
|
||||
placeholder: "1.93.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -40,7 +40,7 @@ body:
|
||||
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: "2.1.0"
|
||||
placeholder: "2.3.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
### 2.3.0
|
||||
|
||||
- [Add documentation page scaling feature](https://github.com/godotengine/godot-vscode-plugin/pull/722)
|
||||
- [Suppress "workspace/symbol" not found error](https://github.com/godotengine/godot-vscode-plugin/pull/723)
|
||||
- [Capitalize the drive letter in Windows absolute paths](https://github.com/godotengine/godot-vscode-plugin/pull/727)
|
||||
|
||||
### 2.2.0
|
||||
|
||||
- [Add partial debugger support for new types (such as typed arrays)](https://github.com/godotengine/godot-vscode-plugin/pull/715)
|
||||
|
||||
@@ -6,7 +6,7 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#map {
|
||||
#minimap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "godot-tools",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "godot-tools",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vscode/debugadapter": "^1.64.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "godot-tools",
|
||||
"displayName": "godot-tools",
|
||||
"icon": "icon.png",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"description": "Tools for game development with Godot Engine and GDScript",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -251,6 +251,13 @@
|
||||
"type": "object",
|
||||
"title": "Godot Tools",
|
||||
"properties": {
|
||||
"godotTools.documentation.pageScale": {
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"minimum": 50,
|
||||
"maximum": 200,
|
||||
"description": "Scale factor (%) to apply to the Godot documentation viewer."
|
||||
},
|
||||
"godotTools.editorPath.godot3": {
|
||||
"type": "string",
|
||||
"default": "godot3",
|
||||
|
||||
@@ -17,13 +17,13 @@ import { subProcess, killSubProcesses } from "../utils/subspawn";
|
||||
const log = createLogger("lsp.manager", { output: "Godot LSP" });
|
||||
|
||||
enum ManagerStatus {
|
||||
INITIALIZING,
|
||||
INITIALIZING_LSP,
|
||||
PENDING,
|
||||
PENDING_LSP,
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
RETRYING,
|
||||
INITIALIZING = 0,
|
||||
INITIALIZING_LSP = 1,
|
||||
PENDING = 2,
|
||||
PENDING_LSP = 3,
|
||||
DISCONNECTED = 4,
|
||||
CONNECTED = 5,
|
||||
RETRYING = 6,
|
||||
}
|
||||
|
||||
export class ClientConnectionManager {
|
||||
@@ -126,16 +126,18 @@ export class ClientConnectionManager {
|
||||
|
||||
if (result.version[2] < minimumVersion) {
|
||||
const message = `Cannot launch headless LSP: Headless LSP mode is only available on v${targetVersion} or newer, but the specified Godot executable is v${result.version}.`;
|
||||
vscode.window.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore").then(item => {
|
||||
if (item === "Select Godot executable") {
|
||||
select_godot_executable(settingName);
|
||||
} else if (item === "Open Settings") {
|
||||
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
|
||||
} else if (item === "Disable Headless LSP") {
|
||||
set_configuration("lsp.headless", false);
|
||||
prompt_for_reload();
|
||||
}
|
||||
});
|
||||
vscode.window
|
||||
.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore")
|
||||
.then((item) => {
|
||||
if (item === "Select Godot executable") {
|
||||
select_godot_executable(settingName);
|
||||
} else if (item === "Open Settings") {
|
||||
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
|
||||
} else if (item === "Disable Headless LSP") {
|
||||
set_configuration("lsp.headless", false);
|
||||
prompt_for_reload();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,7 +199,7 @@ export class ClientConnectionManager {
|
||||
if (this.target === TargetLSP.HEADLESS) {
|
||||
options = ["Restart LSP", ...options];
|
||||
}
|
||||
vscode.window.showInformationMessage(message, ...options).then(item => {
|
||||
vscode.window.showInformationMessage(message, ...options).then((item) => {
|
||||
if (item === "Restart LSP") {
|
||||
this.connect_to_language_server();
|
||||
}
|
||||
@@ -324,7 +326,7 @@ export class ClientConnectionManager {
|
||||
options = ["Open workspace with Godot Editor", ...options];
|
||||
}
|
||||
|
||||
vscode.window.showErrorMessage(message, ...options).then(item => {
|
||||
vscode.window.showErrorMessage(message, ...options).then((item) => {
|
||||
if (item === "Retry") {
|
||||
this.connect_to_language_server();
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import * as vscode from "vscode";
|
||||
import { LanguageClient, NotificationMessage, RequestMessage, ResponseMessage } from "vscode-languageclient/node";
|
||||
import { EventEmitter } from "events";
|
||||
import {
|
||||
LanguageClient,
|
||||
type NotificationMessage,
|
||||
type RequestMessage,
|
||||
type ResponseMessage,
|
||||
} from "vscode-languageclient/node";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { get_configuration, createLogger } from "../utils";
|
||||
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
|
||||
import {
|
||||
type Message,
|
||||
type MessageIO,
|
||||
MessageIOReader,
|
||||
MessageIOWriter,
|
||||
TCPMessageIO,
|
||||
WebSocketMessageIO,
|
||||
} from "./MessageIO";
|
||||
import { globals } from "../extension";
|
||||
|
||||
const log = createLogger("lsp.client", { output: "Godot LSP" });
|
||||
|
||||
export enum ClientStatus {
|
||||
PENDING,
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
PENDING = 0,
|
||||
DISCONNECTED = 1,
|
||||
CONNECTED = 2,
|
||||
}
|
||||
|
||||
export enum TargetLSP {
|
||||
HEADLESS,
|
||||
EDITOR,
|
||||
HEADLESS = 0,
|
||||
EDITOR = 1,
|
||||
}
|
||||
|
||||
const CUSTOM_MESSAGE = "gdscript_client/";
|
||||
|
||||
export default class GDScriptLanguageClient extends LanguageClient {
|
||||
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") === "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
|
||||
public readonly io: MessageIO =
|
||||
get_configuration("lsp.serverProtocol") === "ws" ? new WebSocketMessageIO() : new TCPMessageIO();
|
||||
|
||||
private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
|
||||
private _initialize_request: Message = null;
|
||||
@@ -35,10 +48,14 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
public lastSymbolHovered = "";
|
||||
|
||||
private _started = false;
|
||||
public get started(): boolean { return this._started; }
|
||||
public get started(): boolean {
|
||||
return this._started;
|
||||
}
|
||||
|
||||
private _status: ClientStatus;
|
||||
public get status(): ClientStatus { return this._status; }
|
||||
public get status(): ClientStatus {
|
||||
return this._status;
|
||||
}
|
||||
public set status(v: ClientStatus) {
|
||||
if (this._status !== v) {
|
||||
this._status = v;
|
||||
@@ -72,7 +89,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
// Notify the server about file changes to '.gd files contain in the workspace
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.status = ClientStatus.PENDING;
|
||||
this.io.on("disconnected", this.on_disconnected.bind(this));
|
||||
@@ -149,7 +166,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
let value: string = message.result["contents"]?.value;
|
||||
if (value) {
|
||||
// this is a dirty hack to fix language server sending us prerendered
|
||||
// markdown but not correctly stripping leading #'s, leading to
|
||||
// markdown but not correctly stripping leading #'s, leading to
|
||||
// docstrings being displayed as titles
|
||||
value = value.replace(/\n[#]+/g, "\n");
|
||||
|
||||
@@ -216,7 +233,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
this.io.writer.write(this._initialize_request);
|
||||
}
|
||||
this.status = ClientStatus.CONNECTED;
|
||||
|
||||
|
||||
const host = get_configuration("lsp.serverHost");
|
||||
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
|
||||
}
|
||||
@@ -260,12 +277,12 @@ class MessageHandler extends EventEmitter {
|
||||
|
||||
on_message(message: any) {
|
||||
// FIXME: Hot fix VSCode 1.42 hover position
|
||||
if (message && message.result && message.result.range && message.result.contents) {
|
||||
if (message?.result?.range && message.result.contents) {
|
||||
message.result.range = undefined;
|
||||
}
|
||||
|
||||
// What does this do?
|
||||
if (message && message.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
|
||||
if (message?.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
|
||||
const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length);
|
||||
if (this[method]) {
|
||||
const ret = this[method](message.params);
|
||||
|
||||
@@ -4,34 +4,33 @@
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
|
||||
const DefaultSize: number = 8192;
|
||||
const CR: number = Buffer.from('\r', 'ascii')[0];
|
||||
const LF: number = Buffer.from('\n', 'ascii')[0];
|
||||
const CRLF: string = '\r\n';
|
||||
const CR: number = Buffer.from("\r", "ascii")[0];
|
||||
const LF: number = Buffer.from("\n", "ascii")[0];
|
||||
const CRLF: string = "\r\n";
|
||||
|
||||
export default class MessageBuffer {
|
||||
|
||||
private encoding: BufferEncoding;
|
||||
private index: number;
|
||||
private buffer: Buffer;
|
||||
|
||||
constructor(encoding: string = 'utf8') {
|
||||
constructor(encoding = "utf8") {
|
||||
this.encoding = encoding as BufferEncoding;
|
||||
this.index = 0;
|
||||
this.buffer = Buffer.allocUnsafe(DefaultSize);
|
||||
}
|
||||
|
||||
public append(chunk: Buffer | String): void {
|
||||
var toAppend: Buffer = <Buffer>chunk;
|
||||
if (typeof (chunk) === 'string') {
|
||||
var str = <string>chunk;
|
||||
var bufferLen = Buffer.byteLength(str, this.encoding);
|
||||
public append(chunk: Buffer | string): void {
|
||||
let toAppend: Buffer = <Buffer>chunk;
|
||||
if (typeof chunk === "string") {
|
||||
const str = <string>chunk;
|
||||
const bufferLen = Buffer.byteLength(str, this.encoding);
|
||||
toAppend = Buffer.allocUnsafe(bufferLen);
|
||||
toAppend.write(str, 0, bufferLen, this.encoding);
|
||||
}
|
||||
if (this.buffer.length - this.index >= toAppend.length) {
|
||||
toAppend.copy(this.buffer, this.index, 0, toAppend.length);
|
||||
} else {
|
||||
var newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
|
||||
const newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
|
||||
if (this.index === 0) {
|
||||
this.buffer = Buffer.allocUnsafe(newSize);
|
||||
toAppend.copy(this.buffer, 0, 0, toAppend.length);
|
||||
@@ -42,10 +41,16 @@ export default class MessageBuffer {
|
||||
this.index += toAppend.length;
|
||||
}
|
||||
|
||||
public tryReadHeaders(): { [key: string]: string; } | undefined {
|
||||
let result: { [key: string]: string; } | undefined = undefined;
|
||||
public tryReadHeaders(): { [key: string]: string } | undefined {
|
||||
let result: { [key: string]: string } | undefined = undefined;
|
||||
let current = 0;
|
||||
while (current + 3 < this.index && (this.buffer[current] !== CR || this.buffer[current + 1] !== LF || this.buffer[current + 2] !== CR || this.buffer[current + 3] !== LF)) {
|
||||
while (
|
||||
current + 3 < this.index &&
|
||||
(this.buffer[current] !== CR ||
|
||||
this.buffer[current + 1] !== LF ||
|
||||
this.buffer[current + 2] !== CR ||
|
||||
this.buffer[current + 3] !== LF)
|
||||
) {
|
||||
current++;
|
||||
}
|
||||
// No header / body separator found (e.g CRLFCRLF)
|
||||
@@ -53,18 +58,18 @@ export default class MessageBuffer {
|
||||
return result;
|
||||
}
|
||||
result = Object.create(null);
|
||||
let headers = this.buffer.toString('ascii', 0, current).split(CRLF);
|
||||
headers.forEach((header) => {
|
||||
let index: number = header.indexOf(':');
|
||||
const headers = this.buffer.toString("ascii", 0, current).split(CRLF);
|
||||
for (const header of headers) {
|
||||
const index: number = header.indexOf(":");
|
||||
if (index === -1) {
|
||||
throw new Error('Message header must separate key and value using :');
|
||||
throw new Error("Message header must separate key and value using :");
|
||||
}
|
||||
let key = header.substr(0, index);
|
||||
let value = header.substr(index + 1).trim();
|
||||
const key = header.substr(0, index);
|
||||
const value = header.substr(index + 1).trim();
|
||||
result![key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
let nextStart = current + 4;
|
||||
const nextStart = current + 4;
|
||||
this.buffer = this.buffer.slice(nextStart);
|
||||
this.index = this.index - nextStart;
|
||||
return result;
|
||||
@@ -74,8 +79,8 @@ export default class MessageBuffer {
|
||||
if (this.index < length) {
|
||||
return null;
|
||||
}
|
||||
let result = this.buffer.toString(this.encoding, 0, length);
|
||||
let nextStart = length;
|
||||
const result = this.buffer.toString(this.encoding, 0, length);
|
||||
const nextStart = length;
|
||||
this.buffer.copy(this.buffer, 0, nextStart);
|
||||
this.index = this.index - nextStart;
|
||||
return result;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {
|
||||
AbstractMessageReader,
|
||||
MessageReader,
|
||||
DataCallback,
|
||||
Disposable,
|
||||
RequestMessage,
|
||||
ResponseMessage,
|
||||
NotificationMessage,
|
||||
type MessageReader,
|
||||
type DataCallback,
|
||||
type Disposable,
|
||||
type RequestMessage,
|
||||
type ResponseMessage,
|
||||
type NotificationMessage,
|
||||
AbstractMessageWriter,
|
||||
MessageWriter
|
||||
type MessageWriter,
|
||||
} from "vscode-jsonrpc";
|
||||
import { EventEmitter } from "events";
|
||||
import { WebSocket, Data } from "ws";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { WebSocket, type Data } from "ws";
|
||||
import { Socket } from "net";
|
||||
import MessageBuffer from "./MessageBuffer";
|
||||
import { createLogger } from "../utils";
|
||||
@@ -45,7 +45,6 @@ export class MessageIO extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class WebSocketMessageIO extends MessageIO {
|
||||
private socket: WebSocket = null;
|
||||
|
||||
@@ -59,7 +58,10 @@ export class WebSocketMessageIO extends MessageIO {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = null;
|
||||
const ws = new WebSocket(`ws://${host}:${port}`);
|
||||
ws.on("open", () => { this.on_connected(ws); resolve(); });
|
||||
ws.on("open", () => {
|
||||
this.on_connected(ws);
|
||||
resolve();
|
||||
});
|
||||
ws.on("message", this.on_message.bind(this));
|
||||
ws.on("error", this.on_disconnected.bind(this));
|
||||
ws.on("close", this.on_disconnected.bind(this));
|
||||
@@ -91,7 +93,10 @@ export class TCPMessageIO extends MessageIO {
|
||||
this.socket = null;
|
||||
const socket = new Socket();
|
||||
socket.connect(port, host);
|
||||
socket.on("connect", () => { this.on_connected(socket); resolve(); });
|
||||
socket.on("connect", () => {
|
||||
this.on_connected(socket);
|
||||
resolve();
|
||||
});
|
||||
socket.on("data", this.on_message.bind(this));
|
||||
socket.on("end", this.on_disconnected.bind(this));
|
||||
socket.on("close", this.on_disconnected.bind(this));
|
||||
@@ -162,15 +167,15 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
||||
if (!contentLength) {
|
||||
throw new Error("Header must provide a Content-Length property.");
|
||||
}
|
||||
const length = parseInt(contentLength);
|
||||
if (isNaN(length)) {
|
||||
const length = Number.parseInt(contentLength);
|
||||
if (Number.isNaN(length)) {
|
||||
throw new Error("Content-Length value must be a number.");
|
||||
}
|
||||
this.nextMessageLength = length;
|
||||
// Take the encoding form the header. For compatibility
|
||||
// treat both utf-8 and utf8 as node utf8
|
||||
}
|
||||
var msg = this.buffer.tryReadContent(this.nextMessageLength);
|
||||
const msg = this.buffer.tryReadContent(this.nextMessageLength);
|
||||
if (msg === null) {
|
||||
/** We haven't received the full message yet. */
|
||||
this.setPartialMessageTimer();
|
||||
@@ -179,7 +184,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
||||
this.clearPartialMessageTimer();
|
||||
this.nextMessageLength = -1;
|
||||
this.messageToken++;
|
||||
var json = JSON.parse(msg);
|
||||
const json = JSON.parse(msg);
|
||||
|
||||
log.debug("rx:", json);
|
||||
this.callback(json);
|
||||
@@ -200,13 +205,18 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
||||
if (this._partialMessageTimeout <= 0) {
|
||||
return;
|
||||
}
|
||||
this.partialMessageTimer = setTimeout((token, timeout) => {
|
||||
this.partialMessageTimer = undefined;
|
||||
if (token === this.messageToken) {
|
||||
this.firePartialMessage({ messageToken: token, waitingTime: timeout });
|
||||
this.setPartialMessageTimer();
|
||||
}
|
||||
}, this._partialMessageTimeout, this.messageToken, this._partialMessageTimeout);
|
||||
this.partialMessageTimer = setTimeout(
|
||||
(token, timeout) => {
|
||||
this.partialMessageTimer = undefined;
|
||||
if (token === this.messageToken) {
|
||||
this.firePartialMessage({ messageToken: token, waitingTime: timeout });
|
||||
this.setPartialMessageTimer();
|
||||
}
|
||||
},
|
||||
this._partialMessageTimeout,
|
||||
this.messageToken,
|
||||
this._partialMessageTimeout,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,21 +237,20 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
|
||||
this.io.on("close", () => this.fireClose());
|
||||
}
|
||||
|
||||
public end(): void {
|
||||
|
||||
}
|
||||
public end(): void {}
|
||||
|
||||
public write(msg: Message): Promise<void> {
|
||||
// discard outgoing messages that we know aren't supported
|
||||
if ((msg as RequestMessage).method === "didChangeWatchedFiles") {
|
||||
return;
|
||||
}
|
||||
if ((msg as RequestMessage).method === "workspace/symbol") {
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify(msg);
|
||||
const contentLength = Buffer.byteLength(json, this.encoding);
|
||||
|
||||
const headers: string[] = [
|
||||
ContentLength, contentLength.toString(), CRLF,
|
||||
CRLF
|
||||
];
|
||||
const headers: string[] = [ContentLength, contentLength.toString(), CRLF, CRLF];
|
||||
try {
|
||||
// callback
|
||||
this.io.on_send_message(msg);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
|
||||
import type { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
|
||||
|
||||
export interface NativeSymbolInspectParams {
|
||||
native_class: string;
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as vscode from "vscode";
|
||||
import {
|
||||
Uri,
|
||||
Range,
|
||||
TextDocument,
|
||||
CancellationToken,
|
||||
type TextDocument,
|
||||
type CancellationToken,
|
||||
DocumentLink,
|
||||
DocumentLinkProvider,
|
||||
ExtensionContext,
|
||||
type DocumentLinkProvider,
|
||||
type ExtensionContext,
|
||||
} from "vscode";
|
||||
import { SceneParser } from "../scene_tools";
|
||||
import { convert_resource_path_to_uri, createLogger } from "../utils";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
import type {
|
||||
CancellationToken,
|
||||
CustomDocument,
|
||||
CustomDocumentOpenContext,
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
Uri,
|
||||
WebviewPanel,
|
||||
} from "vscode";
|
||||
import { NotificationMessage } from "vscode-jsonrpc";
|
||||
import {
|
||||
import type { NotificationMessage } from "vscode-jsonrpc";
|
||||
import type {
|
||||
NativeSymbolInspectParams,
|
||||
GodotNativeSymbol,
|
||||
GodotNativeClassInfo,
|
||||
GodotCapabilities,
|
||||
} from "../lsp/gdscript.capabilities";
|
||||
import { make_html_content } from "./documentation_builder";
|
||||
import { createLogger, get_extension_uri, make_docs_uri } from "../utils";
|
||||
import { createLogger, get_configuration, get_extension_uri, make_docs_uri } from "../utils";
|
||||
import { globals } from "../extension";
|
||||
|
||||
const log = createLogger("providers.docs");
|
||||
@@ -37,9 +37,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
},
|
||||
supportsMultipleEditorsPerDocument: true,
|
||||
};
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerCustomEditorProvider("gddoc", this, options),
|
||||
);
|
||||
context.subscriptions.push(vscode.window.registerCustomEditorProvider("gddoc", this, options));
|
||||
}
|
||||
|
||||
public register_capabilities(message: NotificationMessage) {
|
||||
@@ -63,23 +61,28 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
}
|
||||
|
||||
public async list_native_classes() {
|
||||
const classname = await vscode.window.showQuickPick(
|
||||
[...this.classInfo.keys()].sort(),
|
||||
{
|
||||
placeHolder: "Type godot class name here",
|
||||
canPickMany: false,
|
||||
}
|
||||
);
|
||||
const classname = await vscode.window.showQuickPick([...this.classInfo.keys()].sort(), {
|
||||
placeHolder: "Type godot class name here",
|
||||
canPickMany: false,
|
||||
});
|
||||
if (classname) {
|
||||
vscode.commands.executeCommand("vscode.open", make_docs_uri(classname));
|
||||
}
|
||||
}
|
||||
|
||||
public openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): CustomDocument {
|
||||
return { uri: uri, dispose: () => { } };
|
||||
public openCustomDocument(
|
||||
uri: Uri,
|
||||
openContext: CustomDocumentOpenContext,
|
||||
token: CancellationToken,
|
||||
): CustomDocument {
|
||||
return { uri: uri, dispose: () => {} };
|
||||
}
|
||||
|
||||
public async resolveCustomEditor(document: CustomDocument, panel: WebviewPanel, token: CancellationToken): Promise<void> {
|
||||
public async resolveCustomEditor(
|
||||
document: CustomDocument,
|
||||
panel: WebviewPanel,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const className = document.uri.path.split(".")[0];
|
||||
const target = document.uri.fragment;
|
||||
let symbol: GodotNativeSymbol = null;
|
||||
@@ -89,7 +92,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
};
|
||||
|
||||
while (!this.ready) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
symbol = this.symbolDb.get(className);
|
||||
@@ -109,9 +112,12 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
if (!this.htmlDb.has(className)) {
|
||||
this.htmlDb.set(className, make_html_content(panel.webview, symbol, target));
|
||||
}
|
||||
panel.webview.html = this.htmlDb.get(className);
|
||||
|
||||
const scaleFactor = get_configuration("documentation.pageScale");
|
||||
|
||||
panel.webview.html = this.htmlDb.get(className).replaceAll("scaleFactor", scaleFactor);
|
||||
panel.iconPath = get_extension_uri("resources/godot_icon.svg");
|
||||
panel.webview.onDidReceiveMessage(msg => {
|
||||
panel.webview.onDidReceiveMessage((msg) => {
|
||||
if (msg.type === "INSPECT_NATIVE_SYMBOL") {
|
||||
const uri = make_docs_uri(msg.data.native_class, msg.data.symbol_name);
|
||||
vscode.commands.executeCommand("vscode.open", uri);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SymbolKind } from "vscode-languageclient";
|
||||
import * as Prism from "prismjs";
|
||||
import * as csharp from "prismjs/components/prism-csharp";
|
||||
import { marked } from "marked";
|
||||
import { GodotNativeSymbol } from "../lsp/gdscript.capabilities";
|
||||
import type { GodotNativeSymbol } from "../lsp/gdscript.capabilities";
|
||||
import { get_extension_uri } from "../utils";
|
||||
import yabbcode = require("ya-bbcode");
|
||||
|
||||
@@ -14,7 +14,7 @@ const parser = new yabbcode();
|
||||
const wtf = csharp;
|
||||
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
highlight: (code, lang) => {
|
||||
if (lang === "gdscript") {
|
||||
return Prism.highlight(code, GDScriptGrammar, lang);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
|
||||
`;
|
||||
}
|
||||
|
||||
return /*html*/`<!DOCTYPE html>
|
||||
return /*html*/ `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -78,16 +78,16 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
|
||||
|
||||
<title>${symbol.name}</title>
|
||||
</head>
|
||||
<body style="line-height: 16pt;">
|
||||
<body style="line-height: scaleFactor%; font-size: scaleFactor%;">
|
||||
<main>
|
||||
${make_symbol_document(symbol)}
|
||||
</main>
|
||||
|
||||
<canvas id='map'></canvas>
|
||||
<canvas id='minimap'></canvas>
|
||||
|
||||
<script src="${pagemapJsUri}"></script>
|
||||
<script>
|
||||
pagemap(document.querySelector('#map'), ${options});
|
||||
pagemap(document.querySelector('#minimap'), ${options});
|
||||
${initialFocus};
|
||||
|
||||
var vscode = acquireVsCodeApi();
|
||||
@@ -128,128 +128,94 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
|
||||
const ret_type = make_link(parts[2] || "void", undefined);
|
||||
let args = (parts[1] || "").replace(
|
||||
/\:\s([A-z0-9_]+)(\,\s*)?/g,
|
||||
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2"
|
||||
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2",
|
||||
);
|
||||
args = args.replace(/\s=\s(.*?)[\,\)]/g, "");
|
||||
return `${ret_type} ${with_class ? `${classlink}.` : ""}${element(
|
||||
"a",
|
||||
s.name,
|
||||
{ href: `#${s.name}` }
|
||||
)}( ${args} )`;
|
||||
return `${ret_type} ${with_class ? `${classlink}.` : ""}${element("a", s.name, {
|
||||
href: `#${s.name}`,
|
||||
})}( ${args} )`;
|
||||
}
|
||||
|
||||
function make_symbol_elements(
|
||||
s: GodotNativeSymbol,
|
||||
with_class = false
|
||||
): { index?: string; body: string } {
|
||||
function make_symbol_elements(s: GodotNativeSymbol, with_class = false): { index?: string; body: string } {
|
||||
switch (s.kind) {
|
||||
case SymbolKind.Property:
|
||||
case SymbolKind.Variable:
|
||||
{
|
||||
// var Control.anchor_left: float
|
||||
const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const type = make_link(parts[2], undefined);
|
||||
const name = element("a", s.name, { href: `#${s.name}` });
|
||||
const title = element(
|
||||
"h4",
|
||||
`${type} ${with_class ? `${classlink}.` : ""}${s.name}`
|
||||
);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: type + " " + name,
|
||||
body: div,
|
||||
};
|
||||
case SymbolKind.Variable: {
|
||||
// var Control.anchor_left: float
|
||||
const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SymbolKind.Constant:
|
||||
{
|
||||
// const Control.FOCUS_ALL: FocusMode = 2
|
||||
// const Control.NOTIFICATION_RESIZED = 40
|
||||
const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(
|
||||
s.detail
|
||||
);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const type = make_link(parts[3] || "int", undefined);
|
||||
const name = parts[1];
|
||||
const value = element("code", parts[4]);
|
||||
const type = make_link(parts[2], undefined);
|
||||
const name = element("a", s.name, { href: `#${s.name}` });
|
||||
const title = element("h4", `${type} ${with_class ? `${classlink}.` : ""}${s.name}`);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: `${type} ${name}`,
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
case SymbolKind.Constant: {
|
||||
// const Control.FOCUS_ALL: FocusMode = 2
|
||||
// const Control.NOTIFICATION_RESIZED = 40
|
||||
const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const type = make_link(parts[3] || "int", undefined);
|
||||
const name = parts[1];
|
||||
const value = element("code", parts[4]);
|
||||
|
||||
const title = element(
|
||||
"p",
|
||||
`${type} ${with_class ? `${classlink}.` : ""}${name} = ${value}`
|
||||
);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
const title = element("p", `${type} ${with_class ? `${classlink}.` : ""}${name} = ${value}`);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
case SymbolKind.Event: {
|
||||
const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SymbolKind.Event:
|
||||
{
|
||||
const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const args = (parts[2] || "").replace(
|
||||
/\:\s([A-z0-9_]+)(\,\s*)?/g,
|
||||
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2"
|
||||
);
|
||||
const title = element(
|
||||
"p",
|
||||
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
|
||||
}${s.name}( ${args} )`
|
||||
);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
break;
|
||||
const args = (parts[2] || "").replace(
|
||||
/\:\s([A-z0-9_]+)(\,\s*)?/g,
|
||||
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2",
|
||||
);
|
||||
const title = element(
|
||||
"p",
|
||||
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""}${s.name}( ${args} )`,
|
||||
);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
case SymbolKind.Method:
|
||||
case SymbolKind.Function:
|
||||
{
|
||||
const signature = make_function_signature(s, with_class);
|
||||
const title = element("h4", signature);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: signature,
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case SymbolKind.Function: {
|
||||
const signature = make_function_signature(s, with_class);
|
||||
const title = element("h4", signature);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: signature,
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (symbol.kind == SymbolKind.Class) {
|
||||
if (symbol.kind === SymbolKind.Class) {
|
||||
let doc = element("h2", `Class: ${symbol.name}`);
|
||||
if (symbol.class_info.inherits) {
|
||||
const inherits = make_link(symbol.class_info.inherits, undefined);
|
||||
doc += element("p", `Inherits: ${inherits}`);
|
||||
}
|
||||
|
||||
if (symbol.class_info && symbol.class_info.extended_classes) {
|
||||
if (symbol.class_info?.extended_classes) {
|
||||
let inherited = "";
|
||||
for (const c of symbol.class_info.extended_classes) {
|
||||
inherited += (inherited ? ", " : " ") + make_link(c, c);
|
||||
@@ -308,19 +274,18 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
|
||||
add_group("Other Members", others);
|
||||
doc += element("script", `var godot_class = "${symbol.native_class}";`);
|
||||
|
||||
return doc;
|
||||
} else {
|
||||
let doc = "";
|
||||
const elements = make_symbol_elements(symbol, true);
|
||||
if (elements.index) {
|
||||
const symbols: SymbolKind[] = [SymbolKind.Function, SymbolKind.Method];
|
||||
if (!symbols.includes(symbol.kind)) {
|
||||
doc += element("h2", elements.index);
|
||||
}
|
||||
}
|
||||
doc += element("div", elements.body);
|
||||
return doc;
|
||||
}
|
||||
let doc = "";
|
||||
const elements = make_symbol_elements(symbol, true);
|
||||
if (elements.index) {
|
||||
const symbols: SymbolKind[] = [SymbolKind.Function, SymbolKind.Method];
|
||||
if (!symbols.includes(symbol.kind)) {
|
||||
doc += element("h2", elements.index);
|
||||
}
|
||||
}
|
||||
doc += element("div", elements.body);
|
||||
return doc;
|
||||
}
|
||||
|
||||
function element<K extends keyof HTMLElementTagNameMap>(
|
||||
@@ -328,7 +293,7 @@ function element<K extends keyof HTMLElementTagNameMap>(
|
||||
content: string,
|
||||
props = {},
|
||||
new_line?: boolean,
|
||||
indent?: string
|
||||
indent?: string,
|
||||
) {
|
||||
let props_str = "";
|
||||
for (const key in props) {
|
||||
@@ -336,28 +301,26 @@ function element<K extends keyof HTMLElementTagNameMap>(
|
||||
props_str += ` ${key}="${props[key]}"`;
|
||||
}
|
||||
}
|
||||
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""
|
||||
}`;
|
||||
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""}`;
|
||||
}
|
||||
|
||||
function make_link(classname: string, symbol: string) {
|
||||
if (!symbol || symbol == classname) {
|
||||
if (!symbol || symbol === classname) {
|
||||
return element("a", classname, {
|
||||
onclick: `inspect('${classname}')`,
|
||||
href: "",
|
||||
});
|
||||
} else {
|
||||
return element("a", `${classname}.${symbol}`, {
|
||||
onclick: `inspect('${classname}', '${symbol}')`,
|
||||
href: "",
|
||||
});
|
||||
}
|
||||
return element("a", `${classname}.${symbol}`, {
|
||||
onclick: `inspect('${classname}', '${symbol}')`,
|
||||
href: "",
|
||||
});
|
||||
}
|
||||
|
||||
function make_codeblock(code: string, language: string) {
|
||||
const lines = code.split("\n");
|
||||
const indent = lines[0].match(/^\s*/)[0].length;
|
||||
code = lines.map(line => line.slice(indent)).join("\n");
|
||||
code = lines.map((line) => line.slice(indent)).join("\n");
|
||||
return marked.parse(`\`\`\`${language}\n${code}\n\`\`\``);
|
||||
}
|
||||
|
||||
@@ -390,24 +353,21 @@ function format_documentation(bbcode: string, classname: string) {
|
||||
|
||||
html = html.replaceAll("<br/> ", "");
|
||||
// [param <name>]
|
||||
html = html.replaceAll(
|
||||
/\[param\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g,
|
||||
"<code>$1</code>"
|
||||
);
|
||||
html = html.replaceAll(/\[param\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g, "<code>$1</code>");
|
||||
// [method <name>]
|
||||
html = html.replaceAll(
|
||||
/\[method\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g,
|
||||
`<a href="" onclick="inspect('${classname}', '$1')">$1</a>`
|
||||
`<a href="" onclick="inspect('${classname}', '$1')">$1</a>`,
|
||||
);
|
||||
// [<reference>]
|
||||
html = html.replaceAll(
|
||||
/\[(\w+)\]/g,
|
||||
`<a href="" onclick="inspect('$1')">$1</a>` // eslint-disable-line quotes
|
||||
`<a href="" onclick="inspect('$1')">$1</a>`, // eslint-disable-line quotes
|
||||
);
|
||||
// [method <class>.<name>]
|
||||
html = html.replaceAll(
|
||||
/\[\w+\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\.(\w+)\]/g,
|
||||
`<a href="" onclick="inspect('$1', '$2')">$1.$2</a>` // eslint-disable-line quotes
|
||||
`<a href="" onclick="inspect('$1', '$2')">$1.$2</a>`, // eslint-disable-line quotes
|
||||
);
|
||||
|
||||
return html;
|
||||
@@ -466,8 +426,10 @@ const GDScriptGrammar = {
|
||||
punctuation: /\./,
|
||||
},
|
||||
},
|
||||
keyword: /\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
|
||||
builtin: /\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
|
||||
keyword:
|
||||
/\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
|
||||
builtin:
|
||||
/\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
|
||||
boolean: /\b(?:true|false)\b/,
|
||||
number: /(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
|
||||
operator: /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,
|
||||
|
||||
@@ -33,6 +33,10 @@ export async function get_project_dir(): Promise<string | undefined> {
|
||||
}
|
||||
projectFile = file;
|
||||
projectDir = path.dirname(file);
|
||||
if (os.platform() === "win32") {
|
||||
// capitalize the drive letter in windows absolute paths
|
||||
projectDir = projectDir[0].toUpperCase() + projectDir.slice(1);
|
||||
}
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user