Improved syntax, added full word select

This commit is contained in:
2025-07-31 17:42:00 -06:00
parent 889c150b08
commit b362770d90
4 changed files with 264 additions and 343 deletions

View File

@@ -1,7 +1,9 @@
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "!"
"lineComment": {
"comment": "!"
}
},
// symbols used as brackets
"brackets": [

View File

@@ -12,18 +12,33 @@
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"languages": [{
"languages": [
{
"id": "4690basic",
"aliases": ["4690 BASIC", "4690basic"],
"extensions": [".BAS", ".J86"],
"aliases": [
"4690 BASIC",
"4690basic"
],
"extensions": [
".BAS",
".J86"
],
"configuration": "./language-configuration.json"
}],
}
],
"grammars": [
{
"language": "4690basic",
"scopeName": "source.4690basic",
"path": "./syntaxes/4690basic.tmLanguage.json"
}
],
"keybindings": [
{
"command": "4690basic.selectWord",
"key": "ctrl+d",
"when": "editorTextFocus && editorLangId == 4690basic"
}
]
},
"scripts": {

View File

@@ -1,5 +1,10 @@
import * as vscode from 'vscode';
interface FunctionParameterInfo {
name: string;
type: string;
}
interface FunctionDefinition {
name: string;
location: vscode.Location;
@@ -14,7 +19,29 @@ const symbolIndex = new Map<string, FunctionDefinition[]>();
// File index - maps file URIs to their function definitions
const fileIndex = new Map<string, FunctionDefinition[]>();
function getCustomWordRange(document: vscode.TextDocument, position: vscode.Position) {
const line = document.lineAt(position.line);
const text = line.text;
const char = position.character;
const wordPattern = /(\?|[a-zA-Z])([a-zA-Z0-9#\.]*)[$#%]?/g;
let match;
while ((match = wordPattern.exec(text)) !== null) {
const startChar = match.index;
const endChar = match.index + match[0].length;
if (char >= startChar && char <= endChar) {
return new vscode.Range(
new vscode.Position(position.line, startChar),
new vscode.Position(position.line, endChar)
);
}
}
}
export async function activate(context: vscode.ExtensionContext) {
const selector: vscode.DocumentSelector = { language: '4690basic' };
const saveWatcher = vscode.workspace.onDidSaveTextDocument(async (document) => {
@@ -39,6 +66,22 @@ export async function activate(context: vscode.ExtensionContext) {
// Build initial symbol index
await buildInitialSymbolIndex();
const disposable = vscode.commands.registerCommand('4690basic.selectWord', () => {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== '4690basic') return;
const position = editor.selection.active;
const document = editor.document;
const wordRange = getCustomWordRange(document, position);
if (wordRange) {
editor.selection = new vscode.Selection(wordRange.start, wordRange.end);
}
});
// Existing formatter provider (unchanged)
const formatterProvider: vscode.DocumentFormattingEditProvider = {
provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] {
@@ -52,205 +95,178 @@ export async function activate(context: vscode.ExtensionContext) {
let indentLevel = 0;
let i = 0;
// Helper function to remove comments from a line (but preserve line continuations)
function removeComments(line: string): string {
// Helper function to remove comments and continuation markers from a line
function cleanLine(line: string): string {
let inQuotes = false;
let quoteChar = '';
let result = '';
for (let j = 0; j < line.length; j++) {
const char = line[j];
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
result += char;
} else if (inQuotes && char === quoteChar) {
inQuotes = false;
quoteChar = '';
result += char;
} else if (!inQuotes && char === '!') {
return line.substring(0, i).trim();
// Comment starts here, ignore rest of line
break;
} else if (!inQuotes && char === '\\') {
// Continuation marker, ignore rest of line
break;
} else {
result += char;
}
}
return line;
}
// Helper function to check if a line should continue
function shouldContinue(line: string, allLines: string[], currentIndex: number): boolean {
return result.trim();
}
// Helper function to build a complete logical line by looking ahead
function buildLogicalLine(startIndex: number): { logicalLine: string, physicalLines: string[], endIndex: number } {
const physicalLines = [];
let logicalParts = [];
let currentIndex = startIndex;
if (!/(IF|THEN|ENDIF|ELSE|FUNCTION|DEF|SUB|WHILE|FOR)\b/i.test(lines[currentIndex])) {
return { logicalLine: lines[currentIndex], physicalLines: [lines[currentIndex]], endIndex: currentIndex };
}
while (currentIndex < lines.length) {
const line = lines[currentIndex];
const trimmedLine = line.trim();
// Skip comment lines that start with backslash (like \REM!!)
// These should not be treated as continuation lines
if (/^\s*\\(REM|rem)/i.test(trimmedLine)) {
return false;
}
const withoutComments = removeComments(line).trim();
// Check for explicit continuation with \
if (/^(?:[^"\\]|"[^"]*")*\\/.test(withoutComments)) {
// Special case: if this is a THEN \ line, don't continue
// Let the next line be processed as its own logical group
if (/\bTHEN\s*\\/.test(withoutComments)) {
return false;
}
return true;
}
// Check for IF without THEN on the same line
if (/^\s*IF\b/i.test(withoutComments) && !/\bTHEN\b/i.test(withoutComments)) {
return true;
}
// Check if current line looks like a continuation of a condition
if (/^\s*(AND\b|OR\b|NOT\b|\()/i.test(withoutComments) && currentIndex > 0) {
// BUT: if this line contains THEN, it terminates the condition, so it should NOT continue
if (/\bTHEN\b/i.test(withoutComments)) {
return false;
}
// Look back to see if we're in a continuation chain
let prevIndex = currentIndex - 1;
while (prevIndex >= 0) {
const prevLine = removeComments(allLines[prevIndex]).trim();
if (prevLine === '') {
prevIndex--;
if (/^\s*(\\|REM|!)/i.test(trimmedLine)) {
physicalLines.push(line);
currentIndex++;
continue;
}
if (/\\.*$/.test(prevLine) ||
/^\s*IF\b/i.test(prevLine) ||
/^\s*(AND\b|OR\b|NOT\b|\()/i.test(prevLine)) {
return true;
physicalLines.push(line);
const cleanedLine = cleanLine(line);
if (cleanedLine) {
logicalParts.push(cleanedLine);
}
// Check if this line continues (ends with backslash outside quotes/comments)
let hasContinuation = false;
let inQuotes = false;
for (let j = 0; j < line.length; j++) {
const char = line[j];
if (!inQuotes && char === '"') {
inQuotes = true;
} else if (inQuotes && char === '"') {
inQuotes = false;
} else if (!inQuotes && char === '!') {
// Comment starts, no continuation possible after this
break;
} else if (!inQuotes && char === '\\') {
if (!/BEGIN/.test(cleanedLine)) {
hasContinuation = true;
}
const nextLineText = lines[currentIndex + 1];
if (nextLineText && /THEN\s*(!|\\)\\/i.test(cleanedLine) && /^\s*(IF|WHILE|FOR)\b/i.test(nextLineText)) {
hasContinuation = false;
}
break;
}
}
currentIndex++;
// If no continuation marker, this logical line is complete
if (!hasContinuation) {
break;
}
}
const logicalLine = logicalParts.join(' ').trim();
return {
logicalLine,
physicalLines,
endIndex: currentIndex - 1
};
}
// Helper function to check if a logical line should start an indent block
function shouldStartIndentBlock(logicalLine: string): boolean {
if (/^\s*((FUNCTION|DEF|SUB|FOR|WHILE)\b)|(BEGIN\b)/i.test(logicalLine)) {
return true;
}
return false;
}
// Helper function to check if a logical line should end an indent block
function shouldEndIndentBlock(logicalLine: string): boolean {
return /^\s*(END\s+FUNCTION|FEND|END\s+SUB|ENDIF|WEND|NEXT)\b/i.test(logicalLine);
}
// Helper function to check if a logical line is ELSE (which decreases then increases indent)
function isElseStatement(logicalLine: string): boolean {
return /^\s*ELSE\b/i.test(logicalLine) && !/\bBEGIN\b/i.test(logicalLine);
}
while (i < lines.length) {
const originalLine = lines[i];
const trimmedLine = originalLine.trim();
// Skip empty lines
if (trimmedLine === '') {
if (lines[i].trim() === '') {
i++;
continue;
}
// Collect continuation lines
let continuationLines = [originalLine];
let j = i;
// Build complete logical line
const { logicalLine, physicalLines, endIndex } = buildLogicalLine(i);
while (j < lines.length && shouldContinue(lines[j], lines, j)) {
if (j + 1 < lines.length) {
j++;
continuationLines.push(lines[j]);
} else {
break;
}
}
// console.log({ logicalLine, physicalLines, endIndex });
// Build the complete logical line by joining all parts
let logicalParts: string[] = [];
for (let k = 0; k < continuationLines.length; k++) {
let part = continuationLines[k].trim();
part = removeComments(part);
if (part.endsWith('\\')) {
part = part.slice(0, -1).trim();
}
if (part) {
logicalParts.push(part);
}
}
// Determine indentation changes
// const isCompound = isCompoundStatement(logicalLine);
const isElse = isElseStatement(logicalLine);
let shouldEnd = shouldEndIndentBlock(logicalLine);
const shouldStart = shouldStartIndentBlock(logicalLine);
const fullLogical = logicalParts.join(' ').trim();
const firstLineTrimmed = removeComments(continuationLines[0]).trim();
// Check if this line follows a THEN \ continuation
const isPrevThenContinuation = i > 0 && /\bTHEN\s*\\/.test(removeComments(lines[i - 1]));
// Check if this is a THEN continuation that starts a new block
const isThenContinuation = /\bTHEN\s*\\/.test(removeComments(continuationLines[0]));
let thenBlockStarts = false;
if (isThenContinuation && continuationLines.length > 1) {
// Check if the continuation contains an opening construct
for (let k = 1; k < continuationLines.length; k++) {
const contLine = removeComments(continuationLines[k]).trim();
if (/^\s*(FUNCTION|DEF|SUB|WHILE|FOR|IF|BEGIN)\b/i.test(contLine)) {
thenBlockStarts = true;
break;
}
}
}
// Simple approach: look for keywords that affect indentation
const containsBegin = /\bBEGIN\b/i.test(fullLogical);
const containsNext = /^\s*NEXT\b/i.test(firstLineTrimmed);
const isClosing = /^\s*(END\s+FUNCTION|FEND|END\s+SUB|ENDIF|WEND|NEXT)\b/i.test(firstLineTrimmed) ||
containsNext ||
(/^\s*ELSE\b/i.test(firstLineTrimmed) && !containsBegin);
const isOpening = /^\s*(FUNCTION|DEF|SUB|WHILE|FOR)\b/i.test(fullLogical) ||
/^\s*(FUNCTION|DEF|SUB|WHILE|FOR)\b/i.test(firstLineTrimmed) ||
containsBegin ||
thenBlockStarts;
// Handle compound statements like ENDIF ELSE BEGIN
const isCompoundEndifElse = /\bENDIF\b.*\bELSE\s+BEGIN\b/i.test(fullLogical);
const isCompoundEndifElseIf = /\bENDIF\s+ELSE\s+IF\b/i.test(fullLogical) && containsBegin;
// Adjust indent level for closing statements BEFORE formatting
if (isCompoundEndifElse || isCompoundEndifElseIf) {
indentLevel = Math.max(indentLevel - 1, 0);
} else if (isClosing) {
// Adjust indent level BEFORE formatting (for closing statements)
if (isElse || shouldEnd) {
indentLevel = Math.max(indentLevel - 1, 0);
}
// Format all lines in this logical group with current indent level
for (let k = 0; k < continuationLines.length; k++) {
const lineIndex = i + k;
const original = continuationLines[k];
shouldEnd = false;
// Format all physical lines in this logical group
for (let j = 0; j < physicalLines.length; j++) {
const lineIndex = i + j;
const original = physicalLines[j];
const trimmed = original.trim();
if (trimmed === '') continue;
// For continuation lines (k > 0), use appropriate indentation
// Determine indentation for this line
let lineIndentLevel = indentLevel;
if (isPrevThenContinuation) {
// This line follows a THEN \ - it should be indented
lineIndentLevel = indentLevel + 1;
} else if (k > 0 && isThenContinuation) {
// This is a continuation after THEN - it should be indented
lineIndentLevel = indentLevel + 1;
} else if (k > 0) {
// Regular continuation line - check if the previous line ended with \
const prevLine = removeComments(continuationLines[k - 1]).trim();
if (prevLine.endsWith('\\')) {
// Keep the same indentation as the main statement
lineIndentLevel = indentLevel;
}
}
const indent = indentUnit.repeat(lineIndentLevel);
const formatted = indent + trimmed;
if (formatted !== original) {
// if (formatted !== original) {
const lineRange = document.lineAt(lineIndex).range;
edits.push(vscode.TextEdit.replace(lineRange, formatted));
}
// }
}
// Adjust indent level for opening statements AFTER formatting
if (isCompoundEndifElse || isCompoundEndifElseIf) {
indentLevel++;
} else if (isOpening) {
if (shouldStart && !shouldEnd) {
indentLevel++;
}
// Move to the next logical group
i = j + 1;
// Move to next logical group
i = endIndex + 1;
}
return edits;
@@ -262,6 +278,7 @@ export async function activate(context: vscode.ExtensionContext) {
const definitions: FunctionDefinition[] = [];
const lines = document.getText().split('\n');
const identifierRegex = /(\?|[a-zA-Z])([a-zA-Z0-9#\.]*)[\$#%]?/;
const labelRegex = /(\?|[a-zA-Z])([a-zA-Z0-9#\.]*)[\$#%]?:/;
// Helper function to remove comments from a line
function removeComments(line: string): string {
@@ -500,26 +517,26 @@ export async function activate(context: vscode.ExtensionContext) {
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Definition | undefined> {
const wordRange = document.getWordRangeAtPosition(position, /(\?|[a-zA-Z])([a-zA-Z0-9#\.]*)[\$#%]?/);
if (!wordRange) {
const functionWordRange = document.getWordRangeAtPosition(position, /(\?|[a-zA-Z])([a-zA-Z0-9#\.]*)[\$#%]?/);
if (!functionWordRange) {
return undefined;
}
const word = document.getText(wordRange).toLowerCase();
const definitions = symbolIndex.get(word);
const word = document.getText(functionWordRange).toLowerCase();
const functionDefinitions = symbolIndex.get(word);
if (!definitions || definitions.length === 0) {
if (!functionDefinitions || functionDefinitions.length === 0) {
return undefined;
}
// If multiple definitions exist, prefer the one in the current document
const localDef = definitions.find(def => def.location.uri.toString() === document.uri.toString());
const localDef = functionDefinitions.find(def => def.location.uri.toString() === document.uri.toString());
if (localDef) {
return localDef.location;
}
// Otherwise return the first definition found
return definitions[0].location;
return functionDefinitions[0].location;
}
};
@@ -613,16 +630,12 @@ export async function activate(context: vscode.ExtensionContext) {
function findFunctionEnd(startLine: number, functionType: 'FUNCTION' | 'DEF' | 'SUB'): number {
let endPattern: RegExp;
// console.log('functionType: ', functionType);
if (functionType === 'FUNCTION' || functionType === 'DEF') {
endPattern = /^\s*(END\s+FUNCTION)|(FEND)\b/i;
} else { // SUB
endPattern = /^\s*END\s+SUB\b/i;
}
// console.log('endPattern: ', endPattern);
for (let i = startLine + 1; i < lines.length; i++) {
const line = lines[i];
@@ -636,17 +649,12 @@ export async function activate(context: vscode.ExtensionContext) {
continue;
}
// console.log('Evaluating for end: ', line);
// Check if this line matches the end pattern
const endMatch = endPattern.exec(line);
if (endMatch) {
// console.log('Found match: ', endMatch);
// Check if the end keyword is not commented out
if (!isKeywordCommentedOut(line, endMatch)) {
// console.log('Returning i');
return i;
}
}
@@ -683,21 +691,13 @@ export async function activate(context: vscode.ExtensionContext) {
continue;
}
// console.log('Current line: ', trimmedLine);
// Extract the function type and name
const defType = functionMatch[1].toUpperCase() as 'FUNCTION' | 'SUB' | 'DEF';
// console.log('functionMatch: ', functionMatch);
// console.log('Definition type: ', defType);
// Find the identifier after the function keyword
const afterKeyword = line.substring(functionMatch[0].length).trim();
const nameMatch = identifierRegex.exec(afterKeyword);
// console.log('afterKeyword: ', afterKeyword);
// console.log('nameMatch: ', nameMatch);
if (!nameMatch) {
continue;
}
@@ -805,7 +805,8 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.languages.registerDocumentSymbolProvider(selector, documentSymbolProvider),
vscode.languages.registerHoverProvider(selector, hoverProvider),
saveWatcher,
fsWatcher
fsWatcher,
disposable,
);
}

View File

@@ -12,6 +12,10 @@
"name": "string.regexp",
"match": "\\b[\\w\\.]+:"
},
{
"name": "meta",
"match": "\\s:"
},
{
"include": "#function-decl"
},
@@ -42,6 +46,12 @@
{
"include": "#if-statement-end"
},
{
"include": "#else"
},
{
"include": "#then"
},
{
"include": "#for-statement"
},
@@ -407,7 +417,7 @@
},
"logical-operator": {
"name": "keyword.operator.logical",
"match": "(?i)\\b(NOT|AND|OR|XOR)\\b"
"match": "(?i)\\b(NOT|AND|OR|XOR)($|\\n|\\s\\()"
},
"string-operator": {
"name": "keyword.operator",
@@ -513,7 +523,7 @@
},
{
"name": "support.function",
"match": "(?i)\\b(NULL|TRUE|FALSE|READ|WRITE|KEY)\\b[^%\\$\\.]"
"match": "(?i)\\b(NULL|TRUE|FALSE|READ|WRITE|KEY|NOWRITE|NODEL)\\b[^%\\$\\.]"
}
]
},
@@ -633,127 +643,20 @@
}
},
"if-statement": {
"name": "meta.if",
"begin": "(?i)\\b(IF)",
"beginCaptures": {
"0": {
"name": "keyword.control"
}
},
"patterns": [
{
"include": "#math-function"
},
{
"include": "#string-function"
},
{
"include": "#number-literal"
},
{
"include": "#arithmetic-operator"
},
{
"include": "#relational-operator"
},
{
"include": "#logical-operator"
},
{
"include": "#keywords"
},
{
"include": "#strings"
},
{
"name": "keyword.control",
"match": "(?i)THEN"
},
{
"name": "keyword.control",
"match": "(?i)ELSE"
},
{
"name": "keyword.control",
"match": "(?i)(BEGIN)"
},
{
"include": "#identifier"
},
{
"include": "#line-continuation"
},
{
"include": "#comments"
},
{
"match": "\\s+",
"name": "text.whitespace"
}
],
"end": "(?<!\\\\)(\\n)"
"match": "(?i)\\b(IF|BEGIN)\\b"
},
"if-statement-end": {
"name": "meta.if.end",
"begin": "(?i)ENDIF",
"beginCaptures": {
"0": {
"name": "keyword.control"
}
},
"patterns": [
{
"name": "keyword.control",
"match": "ELSE"
"match": "(?i)ENDIF"
},
{
"include": "#math-function"
},
{
"include": "#string-function"
},
{
"include": "#number-literal"
},
{
"include": "#arithmetic-operator"
},
{
"include": "#relational-operator"
},
{
"include": "#logical-operator"
},
{
"include": "#keywords"
},
{
"include": "#strings"
},
{
"name": "keyword.control",
"match": "THEN|then"
},
{
"name": "keyword.control",
"match": "(?i)BEGIN"
},
{
"else": {
"name": "keyword.control",
"match": "(?i)ELSE"
},
{
"then": {
"name": "keyword.control",
"match": "(?i)IF"
},
{
"include": "#line-continuation"
},
{
"include": "#comments"
}
],
"end": "(?<!\\\\)\\n"
"match": "(?i)THEN"
},
"for-statement": {
"name": "meta.for",