UNPKG

chrome-devtools-frontend

Version:
119 lines (103 loc) 3.96 kB
// 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}; } }