diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 035c0e2..095cfe3 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -8,6 +8,7 @@ #include "GBMemoryByteArray.h" #include "GBWarningPopover.h" #include "GBCheatWindowController.h" +#include "GBTerminalTextFieldCell.h" /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */ /* Todo: Split into category files! This is so messy!!! */ @@ -546,6 +547,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) self.debuggerSideViewInput.textColor = [NSColor whiteColor]; self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style; [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"]; + ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &gb; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateSideView) name:NSTextDidChangeNotification @@ -1008,6 +1010,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [debugger_input_queue removeObjectAtIndex:0]; } [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; + if ((id)input == [NSNull null]) { + return NULL; + } return input? strdup([input UTF8String]): NULL; } diff --git a/Cocoa/GBTerminalTextFieldCell.h b/Cocoa/GBTerminalTextFieldCell.h index eae02e5..484e0c3 100644 --- a/Cocoa/GBTerminalTextFieldCell.h +++ b/Cocoa/GBTerminalTextFieldCell.h @@ -1,5 +1,6 @@ #import +#include @interface GBTerminalTextFieldCell : NSTextFieldCell - +@property GB_gameboy_t *gb; @end diff --git a/Cocoa/GBTerminalTextFieldCell.m b/Cocoa/GBTerminalTextFieldCell.m index e95e785..c1ed203 100644 --- a/Cocoa/GBTerminalTextFieldCell.m +++ b/Cocoa/GBTerminalTextFieldCell.m @@ -2,6 +2,7 @@ #import "GBTerminalTextFieldCell.h" @interface GBTerminalTextView : NSTextView +@property GB_gameboy_t *gb; @end @implementation GBTerminalTextFieldCell @@ -12,10 +13,12 @@ - (NSTextView *)fieldEditorForView:(NSView *)controlView { if (field_editor) { + field_editor.gb = self.gb; return field_editor; } field_editor = [[GBTerminalTextView alloc] init]; [field_editor setFieldEditor:YES]; + field_editor.gb = self.gb; return field_editor; } @@ -26,6 +29,8 @@ NSMutableOrderedSet *lines; NSUInteger current_line; bool reverse_search_mode; + NSRange auto_complete_range; + uintptr_t auto_complete_context; } - (instancetype)init @@ -170,6 +175,7 @@ -(void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag { reverse_search_mode = false; + auto_complete_context = 0; [super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag]; } @@ -188,6 +194,38 @@ [attributes setObject:color forKey:NSForegroundColorAttributeName]; [[[NSAttributedString alloc] initWithString:@"Reverse search..." attributes:attributes] drawAtPoint:NSMakePoint(2, 0)]; } - } + +/* Todo: lazy design, make it use a delegate instead of having a gb reference*/ + +- (void)insertTab:(id)sender +{ + if (auto_complete_context == 0) { + NSRange selection = self.selectedRange; + if (selection.length) { + [self delete:nil]; + } + auto_complete_range = NSMakeRange(selection.location, 0); + } + char *substring = strdup([self.string substringToIndex:auto_complete_range.location].UTF8String); + uintptr_t context = auto_complete_context; + char *completion = GB_debugger_complete_substring(self.gb, substring, &context); + free(substring); + if (completion) { + NSString *ns_completion = @(completion); + free(completion); + if (!ns_completion) { + goto error; + } + self.selectedRange = auto_complete_range; + auto_complete_range.length = ns_completion.length; + [self replaceCharactersInRange:self.selectedRange withString:ns_completion]; + auto_complete_context = context; + return; + } +error: + auto_complete_context = context; + NSBeep(); +} + @end diff --git a/Core/debugger.c b/Core/debugger.c index 34144df..038f76f 100644 --- a/Core/debugger.c +++ b/Core/debugger.c @@ -689,6 +689,7 @@ exit: struct debugger_command_s; typedef bool debugger_command_imp_t(GB_gameboy_t *gb, char *arguments, char *modifiers, const struct debugger_command_s *command); +typedef char *debugger_completer_imp_t(GB_gameboy_t *gb, const char *string, uintptr_t *context); typedef struct debugger_command_s { const char *command; @@ -697,6 +698,8 @@ typedef struct debugger_command_s { const char *help_string; // Null if should not appear in help const char *arguments_format; // For usage message const char *modifiers_format; // For usage message + debugger_completer_imp_t *argument_completer; + debugger_completer_imp_t *modifiers_completer; } debugger_command_t; static const char *lstrip(const char *str) @@ -832,6 +835,19 @@ static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const return true; } +static char *on_off_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"on", "off"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + /* Enable or disable software breakpoints */ static bool softbreak(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { @@ -873,6 +889,65 @@ static uint16_t find_breakpoint(GB_gameboy_t *gb, value_t addr) return (uint16_t) min; } +static inline bool is_legal_symbol_char(char c) +{ + if (c >= '0' && c <= '9') return true; + if (c >= 'A' && c <= 'Z') return true; + if (c >= 'a' && c <= 'z') return true; + if (c == '_') return true; + if (c == '.') return true; + return false; +} + +static char *symbol_completer(GB_gameboy_t *gb, const char *string, uintptr_t *_context) +{ + const char *symbol_prefix = string; + while (*string) { + if (!is_legal_symbol_char(*string)) { + symbol_prefix = string + 1; + } + string++; + } + + if (*symbol_prefix == '$') { + return NULL; + } + + struct { + uint16_t bank; + uint32_t symbol; + } *context = (void *)_context; + + + size_t length = strlen(symbol_prefix); + while (context->bank < 0x200) { + if (gb->bank_symbols[context->bank] == NULL || + context->symbol >= gb->bank_symbols[context->bank]->n_symbols) { + context->bank++; + context->symbol = 0; + continue; + } + const char *candidate = gb->bank_symbols[context->bank]->symbols[context->symbol++].name; + if (memcmp(symbol_prefix, candidate, length) == 0) { + return strdup(candidate + length); + } + } + return NULL; +} + +static char *j_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"j"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { bool is_jump_to = true; @@ -1040,6 +1115,19 @@ static uint16_t find_watchpoint(GB_gameboy_t *gb, value_t addr) return (uint16_t) min; } +static char *rw_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"r", "rw", "w"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool watch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { if (strlen(lstrip(arguments)) == 0) { @@ -1277,6 +1365,19 @@ static bool should_break(GB_gameboy_t *gb, uint16_t addr, bool jump_to) return _should_break(gb, full_addr, jump_to); } +static char *format_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"a", "b", "d", "o", "x"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { if (strlen(lstrip(arguments)) == 0) { @@ -1740,6 +1841,19 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg return true; } +static char *wave_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"c", "f", "l"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { if (strlen(lstrip(arguments)) || (modifiers && !strchr("fcl", modifiers[0]))) { @@ -1787,7 +1901,7 @@ static const debugger_command_t commands[] = { {"finish", 1, finish, "Run until the current function returns"}, {"backtrace", 2, backtrace, "Displays the current call stack"}, {"bt", 2, }, /* Alias */ - {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected (Experimental)"}, + {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected"}, {"ticks", 2, ticks, "Displays the number of CPU ticks since the last time 'ticks' was" HELP_NEWLINE "used"}, {"registers", 1, registers, "Print values of processor registers and other important registers"}, @@ -1796,30 +1910,33 @@ static const debugger_command_t commands[] = { {"apu", 3, apu, "Displays information about the current state of the audio chip"}, {"wave", 3, wave, "Prints a visual representation of the wave RAM." HELP_NEWLINE "Modifiers can be used for a (f)ull print (the default)," HELP_NEWLINE - "a more (c)ompact one, or a one-(l)iner", "", "(f|c|l)"}, + "a more (c)ompact one, or a one-(l)iner", "", "(f|c|l)", .modifiers_completer = wave_completer}, {"lcd", 3, lcd, "Displays information about the current state of the LCD controller"}, {"palettes", 3, palettes, "Displays the current CGB palettes"}, - {"softbreak", 2, softbreak, "Enables or disables software breakpoints", "(on|off)"}, + {"softbreak", 2, softbreak, "Enables or disables software breakpoints", "(on|off)", .argument_completer = on_off_completer}, {"breakpoint", 1, breakpoint, "Add a new breakpoint at the specified address/expression" HELP_NEWLINE "Can also modify the condition of existing breakpoints." HELP_NEWLINE "If the j modifier is used, the breakpoint will occur just before" HELP_NEWLINE "jumping to the target.", - "[ if ]", "j"}, - {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[]"}, + "[ if ]", "j", + .argument_completer = symbol_completer, .modifiers_completer = j_completer}, + {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[]", .argument_completer = symbol_completer}, {"watch", 1, watch, "Add a new watchpoint at the specified address/expression." HELP_NEWLINE "Can also modify the condition and type of existing watchpoints." HELP_NEWLINE "Default watchpoint type is write-only.", - "[ if ]", "(r|w|rw)"}, - {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[]"}, + "[ if ]", "(r|w|rw)", + .argument_completer = symbol_completer, .modifiers_completer = rw_completer + }, + {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[]", .argument_completer = symbol_completer}, {"list", 1, list, "List all set breakpoints and watchpoints"}, {"print", 1, print, "Evaluate and print an expression" HELP_NEWLINE "Use modifier to format as an address (a, default) or as a number in" HELP_NEWLINE "decimal (d), hexadecimal (x), octal (o) or binary (b).", - "", "format"}, + "", "format", .argument_completer = symbol_completer, .modifiers_completer = format_completer}, {"eval", 2, }, /* Alias */ - {"examine", 2, examine, "Examine values at address", "", "count"}, + {"examine", 2, examine, "Examine values at address", "", "count", .argument_completer = symbol_completer}, {"x", 1, }, /* Alias */ - {"disassemble", 1, disassemble, "Disassemble instructions at address", "", "count"}, + {"disassemble", 1, disassemble, "Disassemble instructions at address", "", "count", .argument_completer = symbol_completer}, {"help", 1, help, "List available commands or show help for the specified command", "[]"}, @@ -2075,6 +2192,63 @@ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) } } +/* Returns true if debugger waits for more commands */ +char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context) +{ + char *command_string = input; + char *arguments = strchr(input, ' '); + if (arguments) { + /* Actually "split" the string. */ + arguments[0] = 0; + arguments++; + } + + char *modifiers = strchr(command_string, '/'); + if (modifiers) { + /* Actually "split" the string. */ + modifiers[0] = 0; + modifiers++; + } + + const debugger_command_t *command = find_command(command_string); + if (command && command->implementation == help && arguments) { + command_string = arguments; + arguments = NULL; + } + + /* No commands and no modifiers, complete the command */ + if (!arguments && !modifiers) { + size_t length = strlen(command_string); + if (*context >= sizeof(commands) / sizeof(commands[0])) { + return NULL; + } + for (const debugger_command_t *command = &commands[*context]; command->command; command++) { + (*context)++; + if (memcmp(command->command, command_string, length) == 0) { /* Is a substring? */ + return strdup(command->command + length); + } + } + return NULL; + } + + if (command) { + if (arguments) { + if (command->argument_completer) { + return command->argument_completer(gb, arguments, context); + } + return NULL; + } + + if (modifiers) { + if (command->modifiers_completer) { + return command->modifiers_completer(gb, modifiers, context); + } + return NULL; + } + } + return NULL; +} + typedef enum { JUMP_TO_NONE, JUMP_TO_BREAK, diff --git a/Core/debugger.h b/Core/debugger.h index b6a12d9..0678b30 100644 --- a/Core/debugger.h +++ b/Core/debugger.h @@ -34,7 +34,7 @@ bool /* Returns true if debugger waits for more commands. Not relevant for non-G void #endif GB_debugger_execute_command(GB_gameboy_t *gb, char *input); /* Destroys input. */ - +char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context); /* Destroys input, result requires free */ void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path); const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr);