chrome-devtools-frontend
Version:
Chrome DevTools UI
119 lines (103 loc) • 3.96 kB
text/typescript
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js';
const LINE_COMMENT_PATTERN = /^(?:\/\/|#)\s*/gm;
const BLOCK_COMMENT_START_PATTERN = /^\/\*+\s*/;
const BLOCK_COMMENT_END_PATTERN = /\s*\*+\/$/;
const BLOCK_COMMENT_LINE_PREFIX_PATTERN = /^\s*\*\s?/;
export interface CommentNodeInfo {
text: string;
to: number;
}
function findLastNonWhitespacePos(state: CodeMirror.EditorState, cursorPosition: number): number {
const line = state.doc.lineAt(cursorPosition);
const textBefore = line.text.substring(0, cursorPosition - line.from);
const effectiveEnd = line.from + textBefore.trimEnd().length;
return effectiveEnd;
}
function resolveCommentNode(state: CodeMirror.EditorState, cursorPosition: number): CodeMirror.SyntaxNode|undefined {
const tree = CodeMirror.syntaxTree(state);
const lookupPos = findLastNonWhitespacePos(state, cursorPosition);
// Find the innermost syntax node at the last non-whitespace character position.
// The bias of -1 makes it check the character to the left of the position.
const node = tree.resolveInner(lookupPos, -1);
const nodeType = node.type.name;
// Check if the node type is a comment
if (nodeType.includes('Comment')) {
if (!nodeType.includes('BlockComment')) {
return node;
}
// An unclosed block comment can result in the parser inserting an error.
let hasInternalError = false;
tree.iterate({
from: node.from,
to: node.to,
enter: n => {
if (n.type.isError) {
hasInternalError = true;
return false;
}
return true;
},
});
return hasInternalError ? undefined : node;
}
return;
}
function extractBlockCommentText(rawText: string): string|undefined {
// Remove /* and */, whitespace, and common leading asterisks on new lines
if (!rawText.match(BLOCK_COMMENT_START_PATTERN)) {
return;
}
let cleaned = rawText.replace(BLOCK_COMMENT_START_PATTERN, '');
if (!cleaned.match(BLOCK_COMMENT_END_PATTERN)) {
return;
}
cleaned = cleaned.replace(BLOCK_COMMENT_END_PATTERN, '');
// Remove leading " * " from multi-line block comments
cleaned = cleaned.split('\n').map(line => line.replace(BLOCK_COMMENT_LINE_PREFIX_PATTERN, '')).join('\n').trim();
return cleaned;
}
function extractLineComment(node: CodeMirror.SyntaxNode, state: CodeMirror.EditorState): CommentNodeInfo|undefined {
let firstNode = node;
let lastNode = node;
let prev = node.prevSibling;
while (prev?.type.name.includes('LineComment')) {
firstNode = prev;
prev = prev.prevSibling;
}
let next = node.nextSibling;
while (next?.type.name.includes('LineComment')) {
lastNode = next;
next = next.nextSibling;
}
// Extract all lines between the first and last identified node
const fullRawText = state.doc.sliceString(firstNode.from, lastNode.to);
// Process each line to remove prefixes (// or #)
const concatenatedText = fullRawText.replaceAll(LINE_COMMENT_PATTERN, '').replace(/\n\s*\n/g, '\n').trim();
return concatenatedText ? {text: concatenatedText, to: lastNode.to} : undefined;
}
export class AiCodeGenerationParser {
static extractCommentNodeInfo(state: CodeMirror.EditorState, cursorPosition: number): CommentNodeInfo|undefined {
const node = resolveCommentNode(state, cursorPosition);
if (!node) {
return;
}
const nodeType = node.type.name;
const rawText = state.doc.sliceString(node.from, node.to);
let text = '';
if (nodeType.includes('LineComment')) {
return extractLineComment(node, state);
}
if (nodeType.includes('BlockComment')) {
text = extractBlockCommentText(rawText) ?? '';
} else {
text = rawText;
}
if (!Boolean(text)) {
return;
}
return {text, to: node.to};
}
}