├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── editor.h ├── input.c ├── input.h ├── parse.h ├── patterns.c ├── patterns.h ├── pls.c ├── samples ├── Makefile ├── bad.c ├── errors.log ├── proto.rb ├── sample.js ├── simple.txt └── testing-big.txt └── test.c /.gitignore: -------------------------------------------------------------------------------- 1 | pls 2 | .test 3 | *.dSYM 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anton Lindqvist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=pls 2 | VERSION = 0.0.1 3 | 4 | PREFIX ?= /usr/local 5 | UNAME := $(shell uname) 6 | 7 | CCVERSION := $(shell ${CC} --version) 8 | 9 | CFLAGS += -Os -pedantic -std=c99 -Wall -Wextra -Wno-unused-result $(shell pcre-config --cflags) 10 | 11 | # Force colour output 12 | ifneq (,$(findstring LLVM,${CCVERSION})) 13 | CFLAGS += -fcolor-diagnostics 14 | endif 15 | 16 | # OS X ships with the PCRE library but not the development headers 17 | # Linking against the included library allows the binary to be redistributed 18 | ifneq (${UNAME},Darwin) 19 | LDFLAGS += $(shell pcre-config --libs) 20 | endif 21 | 22 | CPPFLAGS += -DVERSION=\"${VERSION}\" -D_POSIX_C_SOURCE=200112L 23 | LDFLAGS += -lpcre 24 | 25 | SOURCES=input.c patterns.c 26 | HEADERS=parse.h input.h patterns.h editor.h 27 | 28 | all: ${NAME} test 29 | 30 | ${NAME}: pls.c ${SOURCES} ${HEADERS} 31 | ${CC} ${CFLAGS} $< ${SOURCES} -o $@ ${LDFLAGS} ${CPPFLAGS} 32 | 33 | .test: test.c ${SOURCES} ${HEADERS} 34 | ${CC} ${CFLAGS} $< ${SOURCES} -o $@ ${LDFLAGS} ${CPPFLAGS} 35 | 36 | test: .test 37 | ./.test 38 | .PHONY: test 39 | 40 | clean: 41 | rm ${NAME} 42 | rm .test 43 | 44 | install: ${NAME} 45 | @echo "${NAME} -> ${PREFIX}/bin/${NAME}" 46 | @mkdir -p "${PREFIX}/bin" 47 | @cp -f ${NAME} "${PREFIX}/bin" 48 | @chmod 755 "${PREFIX}/bin/${NAME}" 49 | @echo "${NAME}.1 -> ${PREFIX}/share/man/man1/${NAME}.1" 50 | @mkdir -p "${PREFIX}/share/man/man1" 51 | @cp -f ${NAME}.1 "${PREFIX}/share/man/man1/${NAME}.1" 52 | @chmod 644 "${PREFIX}/share/man/man1/${NAME}.1" 53 | 54 | .PHONY: clean install 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pls 2 | ==== 3 | 4 | Implore a command to work. 5 | 6 | ![pls](https://raw.githubusercontent.com/ciaran/pls/gh-pages/examples.gif) 7 | 8 | Description 9 | ----------- 10 | 11 | Run a specified command and open printed file/line numbers in your editor. 12 | 13 | `pls` runs the utility given on the command line, and checks each line in the output for a file/line number. On termination of the utility, a file can be selected to open. 14 | 15 | 16 | Examples 17 | -------- 18 | 19 | - Run a `make` build and look for errors: 20 | 21 | ``` 22 | pls make 23 | ``` 24 | 25 | - Run `mocha` tests and ignore system scripts in selection: 26 | 27 | ``` 28 | pls mocha -e 29 | ``` 30 | 31 | - Run a `make` build task and specify a search path for opening files: 32 | 33 | ``` 34 | pls make -p src 35 | ``` 36 | 37 | - Check for errors in a log file: 38 | 39 | ``` 40 | pls < errors.log 41 | ``` 42 | 43 | Compilation 44 | ----------- 45 | 46 | `pls` requires the PCRE development headers, which can be installed using your 47 | system’s package manager, e.g.: 48 | 49 | `brew install pcre` 50 | 51 | `apt-get install libpcre3-dev` 52 | 53 | With PCRE available a simple `make` should suffice. 54 | 55 | Configuration 56 | ------------- 57 | 58 | ### Editor 59 | 60 | By default `pls` will try to infer how to open selected files based on your `$EDITOR` value, falling back on `vim` if it can’t recognise one. 61 | 62 | You can specify an editor command using the `$PLS` environment variable. The first `%d` will be replaced with the line number, and the second `%d` with the column number (or 0 if not available). If a `%s` is present it will be replaced with the filename, otherwise the file will be appended to the end of the command. 63 | 64 | For example: 65 | 66 | - ```vim +'call cursor(%d, %d)'``` 67 | 68 | 69 | ### Patterns 70 | 71 | A few line matching patterns are included by default, but more can be added by creating a `~/.plsrc` file, with one regular expression pattern per line (with no delimiters). 72 | 73 | A pattern should have up to 3 captures – the first being the filename, the second the line number, and the third the column number. 74 | 75 | 76 | Options 77 | ------- 78 | 79 | - `-a`: Show selection interface even if utility exits with 0 status 80 | 81 | - `-l`: Set initial selection to the last matched path 82 | 83 | - `-e`: Only select existing filenames 84 | Useful if build output may include system header files etc. which you don’t want to include in the selection list. 85 | 86 | - `-p`: Add path to the list of directories searched for selected files 87 | This can be used when files may be in include paths. 88 | 89 | 90 | Thanks 91 | ------ 92 | 93 | Includes some code from [yank](https://github.com/mptre/yank), by [Anton Lindqvist](https://github.com/mptre). 94 | -------------------------------------------------------------------------------- /editor.h: -------------------------------------------------------------------------------- 1 | #ifndef EDITOR_H 2 | #define EDITOR_H 3 | 4 | #include 5 | #include 6 | 7 | const char* EDITOR_COMMAND = "vim +'call cursor(%d, %d)'"; 8 | 9 | // is_named_executable("/foo/bar/baz -n1", "baz") -> true 10 | int 11 | is_named_executable (const char* cmd, const char* name) 12 | { 13 | size_t n = BUFSIZ; 14 | char buf[BUFSIZ]; 15 | char* base, *end; 16 | 17 | strcpy(buf, cmd); 18 | 19 | base = basename(buf); 20 | end = strchr(base, ' '); 21 | 22 | if(end) 23 | n = end-base; 24 | 25 | return 0 == strncmp(base, name, n); 26 | } 27 | 28 | // If the PLS variable is set, we format into that 29 | // Otherwise, if EDITOR is set, we try to determine how to use it 30 | // If neither is set we fall back on the default EDITOR_COMMAND. 31 | // This function returns a string using internal static storage. 32 | char* 33 | editor_command () 34 | { 35 | static char cmd[BUFSIZ]; 36 | memset(cmd, 0, BUFSIZ); 37 | 38 | const char* env = getenv("PLS"); 39 | if(!env && (env = getenv("EDITOR"))) 40 | { 41 | // TODO: We don’t want these editor invocations to include --wait, 42 | // so it might be best to just take the executable path only for those cases. 43 | if(is_named_executable(env, "mate") || is_named_executable(env, "mate_wait")) 44 | { 45 | strcpy(cmd, env); 46 | strcat(cmd, " --line %d:%d"); 47 | return cmd; 48 | } 49 | else if (is_named_executable(env, "subl") || is_named_executable(env, "atom")) 50 | { 51 | strcpy(cmd, env); 52 | strcat(cmd, " %s:%d:%d"); 53 | return cmd; 54 | } 55 | else if(is_named_executable(env, "vim") || is_named_executable(env, "vi")) 56 | { 57 | strcpy(cmd, env); 58 | strcat(cmd, " +'call cursor(%d, %d)'"); 59 | return cmd; 60 | } 61 | else if(is_named_executable(env, "phpstorm") || is_named_executable(env, "idea")) 62 | { 63 | strcpy(cmd, env); 64 | strcat(cmd, " --line %d \"$PWD/%s\""); 65 | return cmd; 66 | } 67 | else if(is_named_executable(env, "emacs") || is_named_executable(env, "emacsclient")) 68 | { 69 | strcpy(cmd, env); 70 | strcat(cmd, " +%d:%d"); 71 | return cmd; 72 | } 73 | } 74 | 75 | if(!env) 76 | env = EDITOR_COMMAND; 77 | 78 | strcpy(cmd, env ? env : EDITOR_COMMAND); 79 | 80 | return cmd; 81 | } 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /input.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "input.h" 3 | 4 | // ESC \d+ [;\d+ [; ...]] m 5 | char* 6 | consume_escape_seq (char* c) 7 | { 8 | if(*c != '\033' || c[1] != '[') 9 | return c; 10 | 11 | ++c; 12 | 13 | do 14 | { 15 | ++c; 16 | 17 | while(isdigit(*c)) 18 | ++c; 19 | } 20 | while(*c == ';'); 21 | 22 | ++c; 23 | 24 | return c; 25 | } 26 | 27 | char* 28 | find_next_line (char* line_start, int width) 29 | { 30 | int display_width = 0; 31 | char* c = line_start; 32 | 33 | while(*c != '\0' && display_width < width) 34 | { 35 | if(*c == '\n') 36 | return c+1; 37 | 38 | if(*c == '\033') 39 | { 40 | c = consume_escape_seq(c); 41 | } 42 | else 43 | { 44 | ++c; 45 | ++display_width; 46 | } 47 | } 48 | 49 | if(display_width == width) 50 | return line_start+width; 51 | 52 | return NULL; 53 | } 54 | 55 | size_t 56 | input_read (input_t* input, int fd, int width, int echo) 57 | { 58 | if (input->size == 0) { 59 | fprintf(stderr, "ERROR: read before init\n"); 60 | exit(1); 61 | } 62 | 63 | int n = read(fd, input->v + input->nmemb, input->size - input->nmemb-1); 64 | if (n < 0) { 65 | perror("read"); 66 | return 0; 67 | } 68 | 69 | if (!n) 70 | return 0; 71 | 72 | input->v[input->nmemb + n] = '\0'; 73 | 74 | if(echo) 75 | printf("%s", input->v + input->nmemb); 76 | 77 | char* line_start = input->v + input->line_offsets[input->nlines]; 78 | 79 | while((line_start = find_next_line(line_start, width))) 80 | { 81 | ++input->nlines; 82 | 83 | input->line_offsets[input->nlines] = line_start - input->v; 84 | 85 | if (input->line_offset_size < input->nlines + BUFSIZ) { 86 | input->line_offset_size += BUFSIZ; 87 | input->line_offsets = realloc(input->line_offsets, input->line_offset_size*sizeof(*input->line_offsets)); 88 | } 89 | } 90 | 91 | input->nmemb += n; 92 | 93 | if (input->size < input->nmemb + BUFSIZ) { 94 | input->size *= 2; 95 | input->v = realloc(input->v, input->size); 96 | if (!input->v) 97 | perror("realloc"); 98 | } 99 | return 1; 100 | } 101 | 102 | size_t 103 | find_line_index (input_t* input, size_t offset) 104 | { 105 | size_t i; 106 | for (i = 0; i < input->nlines; ++i) 107 | { 108 | if(input->line_offsets[i] == offset) 109 | return i; 110 | if(i == input->nlines-1) 111 | return i; 112 | if(input->line_offsets[i] < offset && offset < input->line_offsets[i+1]) 113 | return i; 114 | } 115 | fprintf(stderr, "ERROR: Couldn't find line index: %zu\n", offset); 116 | exit(1); 117 | } 118 | 119 | size_t 120 | find_end_offset (input_t* input, size_t stop_offset, size_t height) 121 | { 122 | size_t index = find_line_index(input, stop_offset); 123 | 124 | index += height; 125 | if(index >= input->nlines) 126 | index = input->nlines-1; 127 | 128 | return input->line_offsets[index]; 129 | } 130 | 131 | void 132 | input_init (input_t* input) 133 | { 134 | input->size = BUFSIZ; 135 | input->nmemb = 0; 136 | input->v = malloc(input->size); 137 | if (!input->v) { 138 | perror("malloc"); 139 | exit(1); 140 | } 141 | 142 | input->nlines = 0; 143 | input->line_offset_size = BUFSIZ; 144 | input->line_offsets = calloc(input->line_offset_size, sizeof(*input->line_offsets)); 145 | input->line_offsets[0] = 0; 146 | } 147 | 148 | void 149 | input_free (input_t* input) 150 | { 151 | if(input->size > 0) 152 | free(input->v); 153 | if(input->line_offset_size > 0) 154 | free(input->line_offsets); 155 | } 156 | -------------------------------------------------------------------------------- /input.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_H 2 | #define INPUT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | typedef struct { 10 | size_t size; 11 | size_t nmemb; 12 | 13 | size_t nlines; // number of display lines 14 | 15 | size_t *line_offsets; 16 | size_t line_offset_size; // current size of line_offsets array 17 | 18 | char *v; 19 | } input_t; 20 | 21 | void input_init (input_t* input); 22 | void input_free (input_t* input); 23 | 24 | size_t input_read (input_t* input, int fd, int width, int echo); 25 | 26 | size_t find_line_index (input_t* input, size_t offset); 27 | 28 | size_t find_end_offset (input_t* input, size_t stop_offset, size_t height); 29 | 30 | // (private) 31 | char* find_next_line (char* s, int width); 32 | char* consume_escape_seq (char* c); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /parse.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "patterns.h" 6 | 7 | #define MAX_FIELDS 100 8 | 9 | static size_t field_count = 0; 10 | static struct field_t { 11 | struct { 12 | size_t start; 13 | size_t stop; 14 | } match, path, line, column; 15 | int path_index; 16 | } field_offsets[MAX_FIELDS]; 17 | 18 | typedef int (valid_field_t) (const char* s, struct field_t* field); 19 | 20 | int 21 | match_line (const char* line, int length, size_t offset, pattern_list_t* list, struct field_t* field) 22 | { 23 | int pcreExecRet; 24 | int subStrVec[30]; 25 | int i; 26 | 27 | for (i = 0; i < list->count; ++i) { 28 | pattern_t* pattern = &list->patterns[i]; 29 | 30 | pcreExecRet = pcre_exec(pattern->compiled, pattern->extra, line, length, 0, 0, subStrVec, sizeof(subStrVec) / sizeof(int)); 31 | 32 | if (pcreExecRet > 0) { 33 | if (pcreExecRet == 1) { 34 | fprintf(stderr, "Warning: no captures in match\n"); 35 | continue; 36 | } 37 | 38 | field->match.start = offset+subStrVec[0]; 39 | field->match.stop = offset+subStrVec[1]; 40 | 41 | field->path.start = offset+subStrVec[2]; 42 | field->path.stop = offset+subStrVec[3]; 43 | 44 | if (pcreExecRet > 2) { 45 | field->line.start = offset+subStrVec[4]; 46 | field->line.stop = offset+subStrVec[5]; 47 | } 48 | if (pcreExecRet > 3) { 49 | field->column.start = offset+subStrVec[6]; 50 | field->column.stop = offset+subStrVec[7]; 51 | } 52 | 53 | return 1; 54 | } 55 | } 56 | 57 | return 0; 58 | } 59 | 60 | int study (pattern_list_t* patterns, const char *s, size_t length, valid_field_t* valid_field) 61 | { 62 | const char* line; 63 | char* newline; 64 | size_t offset; 65 | int lineLength; 66 | struct field_t field; 67 | 68 | assert(length > 0); 69 | 70 | field_count = 0; 71 | 72 | for (offset = 0; offset < length; ) { 73 | line = s+offset; 74 | 75 | newline = strchr(line, '\n'); 76 | 77 | if (newline != NULL) { 78 | lineLength = newline - line; 79 | } else { 80 | lineLength = length-offset; 81 | } 82 | 83 | memset(&field, 0, sizeof(field)); 84 | if (match_line(line, lineLength, offset, patterns, &field)) { 85 | if (!valid_field || valid_field(s, &field)) { 86 | field_offsets[field_count] = field; 87 | field_count++; 88 | 89 | if (field_count == MAX_FIELDS) 90 | break; 91 | } 92 | } 93 | 94 | offset += lineLength+1; 95 | } 96 | 97 | return 1; 98 | } 99 | 100 | int 101 | replace (char* out, const char* placeholder, const char* in, int length) 102 | { 103 | char buf[200] = {0}; 104 | char* pos = strstr(out, placeholder); 105 | 106 | if (pos == NULL) 107 | return 0; 108 | 109 | strncat(buf, out, pos-out); 110 | strncat(buf, in, length); 111 | strcat(buf, pos+strlen(placeholder)); 112 | strcpy(out, buf); 113 | 114 | return 1; 115 | } 116 | 117 | int 118 | format_cmd (char* out, const char* s, struct field_t const* field, const char* path) 119 | { 120 | if(field->line.start > 0) 121 | replace(out, "%d", s+field->line.start, field->line.stop-field->line.start); 122 | else replace(out, "%d", "0", 1); 123 | 124 | if(field->column.start > 0) 125 | replace(out, "%d", s+field->column.start, field->column.stop-field->column.start); 126 | else replace(out, "%d", "0", 1); 127 | 128 | char buf[PATH_MAX] = {0}; 129 | if (path) { 130 | strcat(buf, path); 131 | strcat(buf, "/"); 132 | } 133 | strncat(buf, s+field->path.start, field->path.stop-field->path.start); 134 | 135 | if (!replace(out, "%s", buf, PATH_MAX)) { 136 | // If there’s no explicit placeholder for the file path, 137 | // we append it to the end of the command. 138 | size_t len = strlen(out); 139 | if(out[len-1] != ' ') 140 | strcat(out, " "); 141 | strcat(out, buf); 142 | } 143 | 144 | return 1; 145 | } 146 | -------------------------------------------------------------------------------- /patterns.c: -------------------------------------------------------------------------------- 1 | #include "patterns.h" 2 | #include 3 | #include 4 | 5 | const char* default_patterns[] = { 6 | // Handle a possible colour sequence from clang output. 7 | "(?:\033\\[\\dm)?([\\/\\w\\-\\.]+\\.\\w+):(\\d+)(?::(\\d+))?", 8 | "file: ([\\/\\w.]+) line: (\\d+)", 9 | "in (.+?) on line (\\d+)", 10 | "([\\/\\w.]+)\\((\\d+),(\\d+)\\)", 11 | "File \"(.+?)\", line (\\d+)", 12 | }; 13 | 14 | void 15 | init_patterns (pattern_list_t* list) 16 | { 17 | list->count = 0; 18 | } 19 | 20 | void 21 | add_default_patterns (pattern_list_t* list) 22 | { 23 | size_t n; 24 | for(n = 0; n < sizeof(default_patterns)/sizeof(*default_patterns); ++n) 25 | add_pattern(list, default_patterns[n]); 26 | } 27 | 28 | int 29 | add_pattern (pattern_list_t* list, const char* str) 30 | { 31 | const char *pcreErrorStr; 32 | int pcreErrorOffset; 33 | 34 | if (list->count+1 == MAX_PATTERNS) 35 | return 0; 36 | 37 | pattern_t* pattern = &list->patterns[list->count]; 38 | 39 | pattern->str = calloc(strlen(str)+1, sizeof(*str)); 40 | strcpy(pattern->str, str); 41 | 42 | pattern->compiled = pcre_compile(pattern->str, 0, &pcreErrorStr, &pcreErrorOffset, NULL); 43 | if (pcreErrorStr) { 44 | fprintf(stderr, "\033[1mError\033[0m: Could not compile '%s': %s\n", pattern->str, pcreErrorStr); 45 | exit(1); 46 | } 47 | 48 | pattern->extra = pcre_study(pattern->compiled, 0, &pcreErrorStr); 49 | if (pcreErrorStr) { 50 | fprintf(stderr, "\033[1mError\033[0m: Could not study '%s': %s\n", pattern->str, pcreErrorStr); 51 | exit(1); 52 | } 53 | 54 | ++list->count; 55 | 56 | return 1; 57 | } 58 | -------------------------------------------------------------------------------- /patterns.h: -------------------------------------------------------------------------------- 1 | #ifndef PATTERNS_H 2 | #define PATTERNS_H 3 | 4 | #include 5 | 6 | #define MAX_PATTERNS 100 7 | 8 | typedef struct { 9 | char* str; 10 | pcre* compiled; 11 | pcre_extra* extra; 12 | } pattern_t; 13 | 14 | typedef struct { 15 | pattern_t patterns[MAX_PATTERNS]; 16 | int count; 17 | } pattern_list_t; 18 | 19 | void init_patterns (pattern_list_t* list); 20 | 21 | void add_default_patterns (pattern_list_t* list); 22 | 23 | int add_pattern (pattern_list_t* list, const char* str); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /pls.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "parse.h" 14 | #include "input.h" 15 | #include "editor.h" 16 | 17 | static input_t in; 18 | 19 | /* Terminal capabilities */ 20 | #define T_ERASE_DOWN "\033[J" 21 | #define T_COLUMN_ADDRESS "\033[%dG" 22 | #define T_CURSOR_INVISIBLE "\033[?25l" 23 | #define T_CURSOR_UP "\033[%dA" 24 | #define T_CURSOR_VISIBLE "\033[?25h" 25 | #define T_ENTER_CA_MODE "\033[?1049h" 26 | #define T_ENTER_STANDOUT_MODE "\033[7m" 27 | #define T_EXIT_CA_MODE "\033[?1049l" 28 | #define T_RESET_SGR "\033[0m" 29 | 30 | #define ESCAPE 27 31 | #define UP_ARROW 65 32 | #define DOWN_ARROW 66 33 | #define RIGHT_ARROW 67 34 | #define LEFT_ARROW 68 35 | 36 | #define CONTROL(c) (c ^ 0x40) 37 | #define MAX(x, y) (x > y ? x : y) 38 | #define MIN(x, y) (x < y ? x : y) 39 | 40 | static const char **utility; 41 | 42 | static struct { 43 | int in; 44 | int out; 45 | int ca; /* use alternate screen */ 46 | unsigned int height; 47 | unsigned int width; 48 | struct termios attr; 49 | } tty; 50 | 51 | static void args(int, const char **); 52 | static void editor(void); 53 | 54 | static void tend(void); 55 | static void tdraw(const char *s, size_t start, size_t stop); 56 | static void tmain(void); 57 | static void tprintf(const char *, int); 58 | static void tputs(const char *); 59 | static void tsetup(void); 60 | static void twrite(const char *, size_t); 61 | 62 | static ssize_t xwrite(int, const char *, size_t); 63 | 64 | static int selection_index = -1; 65 | 66 | static struct { 67 | int initial_last; // start with last field selected instead of first 68 | int always_select; 69 | int only_existing; 70 | 71 | char* paths[100]; 72 | int path_count; 73 | } options; 74 | 75 | void 76 | args(int argc, const char **argv) 77 | { 78 | int c, i; 79 | 80 | while ((c = getopt(argc, (char * const *) argv, "lavehp:")) != -1) { 81 | switch (c) { 82 | case 'v': 83 | puts("pls " VERSION); 84 | exit(0); 85 | case 'l': 86 | options.initial_last = 1; 87 | break; 88 | case 'a': 89 | options.always_select = 1; 90 | break; 91 | case 'e': 92 | options.only_existing = 1; 93 | break; 94 | case 'p': 95 | options.paths[options.path_count] = optarg; 96 | ++options.path_count; 97 | break; 98 | case 'h': 99 | default: 100 | puts("usage: pls [-lae] [-p path] utility\n"); 101 | if (c == 'h') { 102 | puts("Arguments:" 103 | "\n -e Only select existing filenames" 104 | "\n -l Set initial selection to the last path" 105 | "\n -p Add path to the list of directories searched for selected files" 106 | "\n -a Show selection interface even if utility exits with 0 status" 107 | ); 108 | } 109 | exit(1); 110 | } 111 | } 112 | 113 | // Remaining arguments are invoked as the utility command 114 | utility = calloc(argc - optind + 2, sizeof(const char *)); 115 | if (!utility) 116 | perror("calloc"); 117 | for (i = optind; i < argc; i++) 118 | utility[i - optind] = argv[i]; 119 | } 120 | 121 | void 122 | editor(void) 123 | { 124 | char* cmd = editor_command(); 125 | const char* path = NULL; 126 | const struct field_t* field = &field_offsets[selection_index]; 127 | 128 | if (field->path_index > 0) 129 | path = options.paths[field->path_index-1]; 130 | 131 | format_cmd(cmd, in.v, field, path); 132 | 133 | // vim need stdin to be a tty 134 | (void)freopen("/dev/tty", "r", stdin); 135 | 136 | system(cmd); 137 | } 138 | 139 | ssize_t 140 | xwrite(int fd, const char *s, size_t nmemb) 141 | { 142 | ssize_t r; 143 | size_t n = nmemb; 144 | 145 | do { 146 | r = write(fd, s, n); 147 | if (r < 0) 148 | return r; 149 | n -= r; 150 | s += r; 151 | } while (n); 152 | 153 | return nmemb; 154 | } 155 | 156 | void 157 | tdraw(const char *s, size_t start, size_t stop) 158 | { 159 | static size_t output_start = 0; 160 | static size_t output_stop = 0; 161 | 162 | // Only move the visible region if necessary 163 | if(!(start >= output_start && stop < output_stop)) 164 | { 165 | size_t index = find_line_index(&in, start); 166 | if(index < tty.height/2) 167 | index = 0; 168 | else 169 | index -= tty.height/2; 170 | 171 | output_start = in.line_offsets[index]; 172 | 173 | if(index + tty.height >= in.nlines) 174 | output_stop = in.nmemb; 175 | else 176 | output_stop = in.line_offsets[index+tty.height-1]; 177 | } 178 | 179 | twrite(s+output_start, start-output_start); 180 | tputs(T_ENTER_STANDOUT_MODE); 181 | twrite(s + start, stop - start); 182 | tputs(T_RESET_SGR); 183 | twrite(s + stop, output_stop - stop); 184 | } 185 | 186 | void 187 | tprintf(const char *format, int x) 188 | { 189 | char s[32]; 190 | int n; 191 | 192 | n = snprintf(s, sizeof(s), format, x); 193 | 194 | twrite(s, n); 195 | } 196 | 197 | void 198 | tputs(const char *s) 199 | { 200 | size_t n = strlen(s); 201 | 202 | twrite(s, n); 203 | } 204 | 205 | void 206 | twrite(const char *s, size_t nmemb) 207 | { 208 | if (xwrite(tty.out, s, nmemb) < 0) 209 | perror("write"); 210 | } 211 | 212 | void 213 | tinfo(void) 214 | { 215 | struct winsize ws; 216 | 217 | tty.in = open("/dev/tty", O_RDONLY); 218 | if (!tty.in) 219 | perror("open"); 220 | if (ioctl(tty.in, TIOCGWINSZ, &ws) < 0) { 221 | perror("ioctl"); 222 | tty.height = 24; 223 | tty.width = 80; 224 | } else { 225 | tty.height = ws.ws_row; 226 | tty.width = ws.ws_col; 227 | } 228 | } 229 | 230 | void 231 | tsetup(void) 232 | { 233 | struct termios attr; 234 | 235 | tcgetattr(tty.in, &tty.attr); 236 | memcpy(&attr, &tty.attr, sizeof(struct termios)); 237 | attr.c_lflag &= ~(ICANON|ECHO|ISIG); 238 | tcsetattr(tty.in, TCSANOW, &attr); 239 | 240 | tty.out = open("/dev/tty", O_WRONLY); 241 | if (!tty.out) 242 | perror("open"); 243 | 244 | if (tty.ca) 245 | tputs(T_ENTER_CA_MODE); 246 | tputs(T_CURSOR_INVISIBLE); 247 | } 248 | 249 | void 250 | tend(void) 251 | { 252 | tputs(T_RESET_SGR); 253 | tputs(T_CURSOR_VISIBLE); 254 | tcsetattr(tty.in, TCSANOW, &tty.attr); 255 | 256 | close(tty.in); 257 | close(tty.out); 258 | } 259 | 260 | enum 261 | { 262 | none, 263 | edit, 264 | quit, 265 | prev, next, 266 | first, last, 267 | } 268 | read_command () 269 | { 270 | char c[3]; 271 | 272 | if (read(tty.in, &c, 3) < 0) 273 | perror("read"); 274 | 275 | if (c[0] == ESCAPE) { 276 | if (c[1] != '[') 277 | return none; 278 | 279 | switch (c[2]) { 280 | case 'Z': /* ESC[Z = shift-tab */ 281 | case UP_ARROW: 282 | case LEFT_ARROW: 283 | return prev; 284 | case DOWN_ARROW: 285 | case RIGHT_ARROW: 286 | return next; 287 | default: 288 | return none; 289 | } 290 | } 291 | 292 | switch (c[0]) { 293 | case '\n': 294 | return edit; 295 | case CONTROL('C'): 296 | case CONTROL('D'): 297 | case 'q': 298 | return quit; 299 | case '\t': 300 | case CONTROL('N'): 301 | case 'j': 302 | return next; 303 | case CONTROL('P'): 304 | case 'k': 305 | return prev; 306 | case CONTROL('A'): 307 | return first; 308 | case CONTROL('E'): 309 | return last; 310 | } 311 | 312 | return none; 313 | } 314 | 315 | void 316 | tmain(void) 317 | { 318 | size_t start, stop; 319 | size_t field_index; 320 | 321 | start = stop = 0; 322 | 323 | if(options.initial_last) 324 | field_index = field_count-1; 325 | else 326 | field_index = 0; 327 | 328 | for (;;) { 329 | start = field_offsets[field_index].match.start; 330 | stop = field_offsets[field_index].match.stop; 331 | 332 | tdraw(in.v, start, stop); 333 | 334 | switch (read_command()) { 335 | case edit: 336 | selection_index = field_index; 337 | return; 338 | case quit: 339 | return; 340 | case first: 341 | field_index = 0; 342 | break; 343 | case next: 344 | if (++field_index == field_count) 345 | field_index = 0; 346 | break; 347 | case last: 348 | field_index = field_count-1; 349 | break; 350 | case prev: 351 | if(field_index == 0) 352 | field_index = field_count-1; 353 | else 354 | --field_index; 355 | break; 356 | case none: 357 | continue; 358 | } 359 | 360 | if (in.nlines) 361 | tprintf(T_CURSOR_UP, in.nlines); 362 | tprintf(T_COLUMN_ADDRESS, 1); 363 | tputs(T_ERASE_DOWN); 364 | tputs(T_RESET_SGR); 365 | } 366 | } 367 | 368 | #define PIPES 2 369 | 370 | int 371 | run_utility (void) 372 | { 373 | pid_t pid; 374 | int status; 375 | int filedes[PIPES][2]; 376 | int n; 377 | int max_fd; 378 | fd_set out_fds; 379 | 380 | if(pipe(filedes[0]) == -1 || pipe(filedes[1]) == -1) { 381 | perror("pipe"); 382 | exit(1); 383 | } 384 | 385 | pid = fork(); 386 | if(pid == -1) { 387 | perror("fork"); 388 | exit(1); 389 | } 390 | 391 | if (pid == 0) { 392 | char* const* argv = (char * const*)utility; 393 | 394 | dup2(filedes[0][1], STDOUT_FILENO); 395 | dup2(filedes[1][1], STDERR_FILENO); 396 | 397 | close(filedes[1][0]); 398 | close(filedes[1][1]); 399 | close(filedes[0][0]); 400 | close(filedes[0][1]); 401 | 402 | execvp(argv[0], (char * const*)argv); 403 | perror("execv"); 404 | exit(1); 405 | } 406 | 407 | close(filedes[0][1]); 408 | close(filedes[1][1]); 409 | 410 | while(1) 411 | { 412 | FD_ZERO(&out_fds); 413 | max_fd = 0; 414 | 415 | for(n = 0; n < PIPES; ++n) { 416 | if(filedes[n][0] != 0) { 417 | FD_SET(filedes[n][0], &out_fds); 418 | if(filedes[n][0] > max_fd) 419 | max_fd = filedes[n][0]; 420 | } 421 | } 422 | if(max_fd == 0) 423 | break; 424 | 425 | int count = select(FD_SETSIZE, &out_fds, NULL, NULL, NULL); 426 | if(count == -1) { 427 | perror("select"); 428 | exit(1); 429 | } 430 | if (count > 0) 431 | { 432 | for(n = 0; n < PIPES; ++n) 433 | { 434 | if(FD_ISSET(filedes[n][0], &out_fds)) 435 | { 436 | if(0 == input_read(&in, filedes[n][0], tty.width, 1)) { 437 | close(filedes[n][0]); 438 | filedes[n][0] = 0; 439 | } 440 | } 441 | } 442 | } 443 | } 444 | 445 | waitpid(pid, &status, 0); 446 | 447 | // TODO: there is more to check here than > 0, 448 | // `man 3 wait` for details. 449 | 450 | return status; 451 | } 452 | 453 | void 454 | readrc(pattern_list_t* list) 455 | { 456 | char line[255]; 457 | wordexp_t exp_result; 458 | wordexp("~/.plsrc", &exp_result, 0); 459 | 460 | const char* path = exp_result.we_wordv[0]; 461 | 462 | if(0 != access(path, F_OK)) 463 | return; 464 | 465 | FILE* fd = fopen(path, "r"); 466 | 467 | while(fgets(line, 255, fd)) 468 | { 469 | size_t len = strlen(line); 470 | if(len <= 2) // Lazy blank line test 471 | continue; 472 | 473 | if(line[0] == '#') 474 | continue; 475 | 476 | if(line[len-1] == '\n') 477 | line[len-1] = '\0'; 478 | 479 | add_pattern(list, line); 480 | } 481 | 482 | fclose(fd); 483 | } 484 | 485 | int 486 | exists (const char* path, size_t len) 487 | { 488 | char buf[PATH_MAX]; 489 | strncpy(buf, path, len); 490 | buf[len] = '\0'; 491 | 492 | return 0 == access(buf, F_OK); 493 | } 494 | 495 | int valid_field (const char* s, struct field_t* field) 496 | { 497 | if (!options.only_existing) 498 | return 1; 499 | 500 | const char* path = s+field->path.start; 501 | size_t length = field->path.stop-field->path.start; 502 | char buf[PATH_MAX] = {0}; 503 | int n; 504 | 505 | strncpy(buf, path, length); 506 | 507 | if(0 == access(buf, F_OK)) 508 | return 1; 509 | 510 | for (n = 0; n < options.path_count; ++n) 511 | { 512 | strcpy(buf, options.paths[n]); 513 | strcat(buf, "/"); 514 | strncat(buf, path, length); 515 | 516 | if(0 == access(buf, F_OK)) { 517 | field->path_index = n+1; 518 | return 1; 519 | } 520 | } 521 | 522 | return 0; 523 | } 524 | 525 | int 526 | main(int argc, const char *argv[]) 527 | { 528 | if (!isatty(fileno(stdout))) { 529 | fprintf(stderr, "\033[1mError\033[0m: output is not a terminal\n"); 530 | exit(1); 531 | } 532 | 533 | pattern_list_t patterns; 534 | patterns.count = 0; 535 | init_patterns(&patterns); 536 | add_default_patterns(&patterns); 537 | 538 | readrc(&patterns); 539 | 540 | args(argc, argv); 541 | 542 | if (patterns.count == 0) { 543 | fprintf(stderr, "\033[1mError\033[0m: no patterns loaded!\n"); 544 | exit(1); 545 | } 546 | 547 | tinfo(); 548 | 549 | input_init(&in); 550 | if(argc - optind > 0) { 551 | if (run_utility() == 0 && !options.always_select) 552 | exit(0); 553 | } else { 554 | while(input_read(&in, STDIN_FILENO, tty.width, 1)) 555 | ; 556 | } 557 | 558 | if(in.nmemb == 0) 559 | exit(0); 560 | 561 | study(&patterns, in.v, in.nmemb, &valid_field); 562 | 563 | if (field_count == 0) 564 | return 0; 565 | 566 | tsetup(); 567 | 568 | // Since we echo the input as we receive it, 569 | // we need to rewind back up to the start. 570 | if (in.nlines) 571 | tprintf(T_CURSOR_UP, MIN(in.nlines, tty.height)); 572 | tprintf(T_COLUMN_ADDRESS, 1); 573 | tputs(T_ERASE_DOWN); 574 | 575 | tmain(); 576 | tend(); 577 | 578 | if (selection_index >= 0) 579 | editor(); 580 | 581 | return 0; 582 | } 583 | -------------------------------------------------------------------------------- /samples/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS += -fcolor-diagnostics 2 | 3 | bad: 4 | @cc ${CFLAGS} bad.c 5 | -------------------------------------------------------------------------------- /samples/bad.c: -------------------------------------------------------------------------------- 1 | int main(int argc, char const *argv[]) 2 | { 3 | foo; 4 | return 0; 5 | } 6 | -------------------------------------------------------------------------------- /samples/errors.log: -------------------------------------------------------------------------------- 1 | 1 2 | asdas sample.js:12 3 | 4 | foo bar sample.js:16 baz 5 | 6 | foo bar sample.js:6:4 baz 7 | 8 | foo bar sample.js:19 baz 9 | 10 | asdasd file: /foo/bar/baz line: 19 asdasd 11 | -------------------------------------------------------------------------------- /samples/proto.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "pp" 3 | require "colored" 4 | require "tty-screen" 5 | 6 | Match = Struct.new(:line_index, :start_offset, :stop_offset, :match) 7 | 8 | term_width, term_height = TTY::Screen.width, TTY::Screen.height 9 | # term_width = 80 10 | 11 | patt = %r{test/hash-fs.spec.js:26:8|sample.js:(\d+)} 12 | 13 | T_ENTER_STANDOUT_MODE = "\033[7m" 14 | T_EXIT_CA_MODE = "\033[?1049l" 15 | T_EXIT_STANDOUT_MODE = "\033[0m" 16 | 17 | T_CURSOR_UP = "\033[%dA" 18 | T_COLUMN_ADDRESS = "\033[%dG" 19 | T_ERASE_LINE = "\033[2K" # erase line 20 | T_ERASE_DOWN = "\033[J" # erase from current line to bottom of screen 21 | 22 | data = '' 23 | 24 | line_start_offsets = [] 25 | text_line_offsets = [] 26 | matched_lines = [] 27 | selected_line = nil 28 | 29 | f = File.open('testing-big.txt', 'r') 30 | # f = File.open('errors.log', 'r') 31 | 32 | while line = f.gets(term_width) 33 | abort "This shouldn't happen" if line.size > term_width 34 | 35 | if nl = line.index("\n") 36 | text_line_offsets << data.size+nl 37 | else 38 | # line_start_offsets << data.size 39 | end 40 | line_start_offsets << data.size 41 | 42 | if match = patt.match(line) 43 | matched_lines << Match.new( 44 | text_line_offsets.size, 45 | data.size + match.begin(0), 46 | data.size + match.end(0), 47 | match 48 | ) 49 | end 50 | 51 | data << line 52 | end 53 | 54 | abort "No matches" if matched_lines.empty? 55 | 56 | def start_index_for_selection_focus(start_offset, stop_offset, sel, line_start_offsets, height) 57 | if stop_offset.nil? or not (sel.start_offset >= start_offset and sel.stop_offset < stop_offset) 58 | selection_line_index = find_line_index(sel.start_offset, line_start_offsets) 59 | 60 | index = selection_line_index - height/2 61 | index = 0 if index < 0 62 | 63 | start_offset = line_start_offsets[index] 64 | 65 | stop_offset = find_end_offset(start_offset, line_start_offsets, height) 66 | end 67 | 68 | return start_offset, stop_offset 69 | end 70 | 71 | def find_line_index(offset, line_start_offsets) 72 | lines = line_start_offsets.size 73 | lines.times do |n| 74 | return n if line_start_offsets[n] == offset 75 | return n if line_start_offsets[n] < offset and offset < line_start_offsets[n+1] 76 | end 77 | nil 78 | # line_start_offsets.find { |i| i == start_offset } 79 | end 80 | 81 | # p find_line_index(64, line_start_offsets) 82 | 83 | # abort "error" unless find_line_index(1, line_start_offsets) == 1 84 | # abort "error" unless find_line_index(3, line_start_offsets) == 1 85 | # abort "error" unless find_line_index(64, line_start_offsets) == 6 86 | # abort "error" unless find_line_index(531, line_start_offsets) == 17 87 | # exit 88 | 89 | def find_end_offset(start_offset, line_start_offsets, height) 90 | index = find_line_index(start_offset, line_start_offsets) 91 | 92 | index += height 93 | index = line_start_offsets.size-1 if index >= line_start_offsets.size 94 | 95 | line_start_offsets[index] 96 | end 97 | 98 | def getc 99 | begin 100 | system("stty raw -echo") 101 | str = STDIN.getc 102 | ensure 103 | system("stty -raw echo") 104 | end 105 | end 106 | 107 | selected_index = 0 108 | start_offset = 0 109 | stop = nil 110 | 111 | while true 112 | sel = matched_lines[selected_index] 113 | pp sel 114 | 115 | start_offset, stop = start_index_for_selection_focus(start_offset, stop, sel, line_start_offsets, term_height) 116 | p [start_offset, stop] 117 | exit 118 | 119 | # stop, lines = end_offset(start_offset, data, term_width, term_height) 120 | # stop = find_end_offset(start_offset, line_start_offsets, term_height) 121 | 122 | if sel.start_offset >= start_offset && sel.start_offset <= stop 123 | print data[start_offset...sel.start_offset] 124 | print T_ENTER_STANDOUT_MODE 125 | print data[sel.start_offset...sel.stop_offset] 126 | print T_EXIT_STANDOUT_MODE 127 | print data[sel.stop_offset...stop] 128 | else 129 | print data[start_offset...stop] 130 | end 131 | 132 | case getc 133 | when 'n'; selected_index += 1 134 | when 'p'; selected_index -= 1 135 | else 136 | exit 137 | end 138 | 139 | if selected_index == matched_lines.size 140 | selected_index = 0 141 | elsif selected_index == -1 142 | selected_index = matched_lines.size-1 143 | end 144 | 145 | printf T_CURSOR_UP, [line_start_offsets.size-1, term_height].min 146 | printf T_COLUMN_ADDRESS, 1 147 | print T_ERASE_DOWN 148 | end 149 | -------------------------------------------------------------------------------- /samples/sample.js: -------------------------------------------------------------------------------- 1 | /* jshint undef: true */ 2 | 3 | (function(){ 4 | var foo; 5 | 6 | bar(); 7 | 8 | "hi" 9 | })(); 10 | -------------------------------------------------------------------------------- /samples/simple.txt: -------------------------------------------------------------------------------- 1 | foobar.js:16 baz 2 | 3 | asdas pls.c:125:18:  4 | 5 | foo bar foobar.js:14:6 baz 6 | 7 | foo bar foobar.js:19 baz 8 | 9 | asdasd file: /foo/bar/baz line: 19 asdasd 10 | -------------------------------------------------------------------------------- /samples/testing-big.txt: -------------------------------------------------------------------------------- 1 | 2 | == Setting up Platform == 3 | 4 | == path == 5 | browserified 6 | == fs == 7 | html5 8 | == crypto == 9 | html5 10 | == process == 11 | html5 12 | 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa laborum repellendus exercitationem modi deleniti et illum rerum saepe ea eaque vitae nulla, nemo magnam sunt provident praesentium excepturi fugiat neque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa laborum repellendus exercitationem modi deleniti et illum rerum saepe ea eaque vitae nulla, nemo magnam sunt provident praesentium excepturi fugiat neque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa laborum repellendus exercitationem modi deleniti et illum rerum saepe ea eaque vitae nulla, nemo magnam sunt provident praesentium excepturi fugiat neque. 14 | 15 | at Context. (test/hash-fs.spec.js:26:8) 16 | 17 | == update == 18 | { exporting: 'cobie', 19 | format: 'excel', 20 | sheets: 21 | [ 'Contact', 22 | 'Facility', 23 | 'Floor', 24 | 'Space', 25 | 'Zone', 26 | 'Type', 27 | 'Component', 28 | 'System', 29 | 'Assembly', 30 | 'Connection', 31 | 32 | at Context. (test/hash-fs.spec.js:26:8) 33 | 34 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa laborum repellendus exercitationem modi deleniti et illum rerum saepe ea eaque vitae nulla, nemo magnam sunt provident praesentium excepturi fugiat neque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa laborum repellendus exercitationem modi deleniti et illum rerum saepe ea eaque vitae nulla, nemo magnam sunt provident praesentium excepturi fugiat neque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa laborum repellendus exercitationem modi deleniti et illum rerum saepe ea eaque vitae nulla, nemo magnam sunt provident praesentium excepturi fugiat neque. 35 | 36 | 37 | 'Spare', 38 | 'Resource', 39 | 'Job', 40 | 'Impact', 41 | 'Document', 42 | 'Attribute', 43 | 'Coordinate', 44 | 'Issue', 45 | 'Picklist' ] } 46 | { COBie: 'building', sheet: 'Document', is: '0 of 4' } 47 | { COBie: 'building', sheet: 'Contact', is: '1 of 4' } 48 | { COBie: 'building', sheet: 'Picklist', is: '2 of 4' } 49 | { COBie: 'building', sheet: 'Component', is: '3 of 4' } 50 | 51 | 52 | at Context. (test/hash-fs.spec.js:26:8) 53 | 54 | catalog sync api 55 | 56 | ✓ Conflicting entries do not get counted as unsynchronised 57 | 58 | catalog client api firewall 59 | ENTRY: view allowed undefined CAN create 60 | 61 | ✓ can add an entry that the user DOES have permission for 62 | ENTRY: view forbidden undefined CAN NOT create 63 | 64 | ✓ fails to add an entry that the user does NOT have permission for 65 | ENTRY: view allowed undefined CAN create 66 | ENTRY: view allowed cdb9c474-6876-44b4-88d3-145fc81a92d8 CAN update 67 | 68 | ✓ can modify an entry that the user DOES have permission for 69 | ENTRY: view allowed undefined CAN create 70 | ENTRY: view allowed 0b6fa2a4-5a85-42ae-adf7-7cc8bf2b5198 CAN NOT update 71 | 72 | ✓ fails to modify an entry that the user does NOT have permission for 73 | ENTRY: view allowed undefined CAN create 74 | ENTRY: view allowed cddc07f4-e6bd-42f9-a7b2-5b0f7b6394ff CAN NOT delete 75 | 76 | ✓ fails to delete the entry 77 | 78 | catalog client api 79 | 80 | ✓ Adds inline entries 81 | 82 | ✓ Adds resource based entries 83 | 84 | ✓ Adds json-as-resource entries 85 | 86 | ✓ Add with no name gives error 87 | 88 | ✓ Add inline with no data gives error 89 | 90 | ✓ Add resource with no path gives error 91 | 92 | ✓ Add json-as-resource with no data gives error 93 | 94 | ✓ Adds entry with a ref, ref returned by get 95 | 96 | ✓ Add trims empty ref 97 | 98 | ✓ Add trims empty ref elements 99 | 100 | ✓ Add does not add parent folder if ref.folder IS an ID 101 | 102 | ✓ Add does not add parent folder if ref.folder has no name 103 | 104 | ✓ modifies inline entry metadata, data not changed 105 | 106 | ✓ modifies inline entry data, metadata not changed 107 | 108 | ✓ modifies resource entry metadata, resource not changed 109 | 110 | ✓ modifies resource entry resource, metadata not changed 111 | 112 | ✓ modifies resource-as-json entry metadata, data not changed 113 | 114 | ✓ modifies resource-as-json entry data, metadata not changed 115 | 116 | ✓ modify entry ignores things it should ignore 117 | 118 | ✓ modify entry does not add parent folder if ref.folder IS an ID 119 | 120 | ✓ gets inline entry 121 | 122 | ✓ gets resource entry with path 123 | 124 | ✓ gets resource-as-json entry with data 125 | 126 | ✓ get projects folder-path for items in nested folders 127 | 128 | ✓ removes inline entries 129 | 130 | ✓ removes resource entries 131 | 132 | ✓ removes resource-as-json entries 133 | 134 | ✓ creates docs from templates 135 | 136 | ✓ errors on invalid entry 137 | 138 | ✓ rejects folder cycles 139 | 140 | ✓ moveToFolder rejects folder cycles 141 | ref.obj undefined 142 | entry { id: 'cad2bbfb-94cb-4cb7-98e8-bdf096777e99', 143 | type: 'folder', 144 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.825Z' }, 145 | sync: { local: { state: 'added' } }, 146 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.825Z' }, 147 | name: 'a', 148 | data: {} } 149 | at Context. (test/hash-fs.spec.js:26:8) 150 | 151 | ✓ addReferenceToEntry rejects folder cycles 152 | 153 | ✓ setReferenceOfEntry rejects folder cycles 154 | ref.obj { newMId: [ 'n1', 'n2' ] } 155 | entry { id: 'd4596ae3-b4c7-4b28-9a49-cf6e92196363', 156 | type: 'view', 157 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.839Z' }, 158 | sync: { local: { state: 'added' } }, 159 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.839Z' }, 160 | name: 'obj', 161 | data: {}, 162 | ref: { obj: { newMId: [Object] } } } 163 | ref.obj undefined 164 | entry { id: 'dd900edd-2225-47b3-98b0-ad7a2ac0c9dc', 165 | type: 'view', 166 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.841Z' }, 167 | sync: { local: { state: 'added' } }, 168 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.841Z' }, 169 | name: 'id', 170 | data: {}, 171 | ref: { id: [ 'n1', 'n2' ] } } 172 | ref.obj undefined 173 | entry { id: '05f84eb5-9cfc-47c2-a74d-0bad1463fcf8', 174 | type: 'view', 175 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.843Z' }, 176 | sync: { local: { state: 'added' } }, 177 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.843Z' }, 178 | name: 'folder', 179 | data: {} } 180 | ref.obj undefined 181 | entry { id: 'a7ff19a3-389d-46a7-b1db-4873108d2bc1', 182 | type: 'view', 183 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.848Z' }, 184 | sync: { local: { state: 'added' } }, 185 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.848Z' }, 186 | name: 'folderAliases', 187 | data: {}, 188 | ref: { folderAliases: [ 'n1', 'n2' ] } } 189 | ref.obj undefined 190 | entry { id: '93d14f2f-be90-4a0c-9d54-56fc3ab1ef4b', 191 | type: 'view', 192 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.845Z' }, 193 | sync: { local: { state: 'added' } }, 194 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.845Z' }, 195 | name: 'label', 196 | data: {}, 197 | ref: { label: [ 'n1', 'n2' ] } } 198 | ref.obj undefined 199 | entry { id: '140c8732-27ad-48e6-9670-0d94905a5b6c', 200 | type: 'view', 201 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.846Z' }, 202 | sync: { local: { state: 'added' } }, 203 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.846Z' }, 204 | name: 'template', 205 | data: {} } 206 | 207 | ✓ adds references via addReferenceToEntry when entry has no existing ref 208 | ref.obj { newMId: [ 'n1', 'n2' ] } 209 | entry { id: 'c4446738-0316-4829-b831-1f5455b3b32f', 210 | type: 'view', 211 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.880Z' }, 212 | sync: { local: { state: 'added' } }, 213 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.880Z' }, 214 | name: 'obj', 215 | data: {}, 216 | ref: { obj: { mid1: [Object], mid2: [Object], newMId: [Object] } } } 217 | ref.obj undefined 218 | entry { id: '62248528-3c03-4c5b-88de-f59b8966b635', 219 | type: 'view', 220 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.882Z' }, 221 | sync: { local: { state: 'added' } }, 222 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.882Z' }, 223 | name: 'id', 224 | data: {}, 225 | ref: { id: [ 'id1', 'id2', 'n1', 'n2' ] } } 226 | ref.obj undefined 227 | 228 | 229 | at Context. (test/hash-fs.spec.js:26:8) 230 | 231 | entry { id: '342b418b-5d2a-425f-8c46-bd1183ec40b3', 232 | type: 'view', 233 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.884Z' }, 234 | sync: { local: { state: 'added' } }, 235 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.884Z' }, 236 | name: 'folder', 237 | data: {}, 238 | ref: { folder: 'id1' } } 239 | ref.obj undefined 240 | entry { id: '73810c94-d8fd-4ff2-9970-54eb4b0dd526', 241 | type: 'view', 242 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.890Z' }, 243 | sync: { local: { state: 'added' } }, 244 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.890Z' }, 245 | name: 'folderAliases', 246 | data: {}, 247 | ref: { folderAliases: [ 'id1', 'id2', 'n1', 'n2' ] } } 248 | ref.obj undefined 249 | entry { id: '84aa77ee-285f-47d6-b9fd-f1000ba9b978', 250 | type: 'view', 251 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.886Z' }, 252 | sync: { local: { state: 'added' } }, 253 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.886Z' }, 254 | name: 'label', 255 | data: {}, 256 | ref: { label: [ 'id1', 'id2', 'n1', 'n2' ] } } 257 | ref.obj undefined 258 | entry { id: '50bb3eb8-5066-4da9-aca7-8025b7db1330', 259 | type: 'view', 260 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.888Z' }, 261 | sync: { local: { state: 'added' } }, 262 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.888Z' }, 263 | name: 'template', 264 | data: {}, 265 | ref: { template: 'id1' } } 266 | 267 | ✓ adds references via addReferenceToEntry when entry has existing ref (43ms) 268 | ref.obj undefined 269 | entry { id: 'c9d9a098-0610-4d71-aa61-22690f050143', 270 | type: 'view', 271 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.937Z' }, 272 | sync: { local: { state: 'added' } }, 273 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:34.937Z' }, 274 | name: 'folderAliases', 275 | data: {}, 276 | ref: { folderAliases: [ 'id1', 'id2' ], id: [ 'n1', 'n2' ] } } 277 | 278 | ✓ merges new refs when addReferenceToEntry 279 | 280 | ✓ removes references via removeReferenceFromEntry (42ms) 281 | 282 | ✓ does nothing when removeReferenceFromEntry with no references (38ms) 283 | 284 | ✓ prunes ref.thing when removeReferenceFromEntry removes all thing (64ms) 285 | 286 | ✓ Does not prune ref if other things present on removeReferenceFromEntry 287 | 288 | ✓ Prunes ref if empty on removeReferenceFromEntry 289 | 290 | ✓ overwrites only the ref specified on setReferenceForEntry (39ms) 291 | 292 | ✓ adds only the ref specified on setReferenceForEntry 293 | 294 | ✓ prunes only the ref specified on setReferenceForEntry (48ms) 295 | 296 | ✓ prunes ref if empty after setReferenceForEntry 297 | 298 | ✓ compares file resources 299 | 300 | ✓ allows docs to be added to root explicitly 301 | 302 | ✓ allows folders to be added to root explicitly 303 | at Context. (test/hash-fs.spec.js:26:8) 304 | 305 | ✓ allows docs to be added to root implicitly 306 | 307 | ✓ allows folders to be added to root implicitly 308 | 309 | ✓ moves entries from one folder to another 310 | 311 | ✓ removes folder contents recursively (90ms) 312 | 313 | ✓ flagDocumentAsEdited 314 | 315 | ✓ sets default view if no settings 316 | 317 | ✓ sets default view if settings 318 | 319 | ✓ gets no default view if no settings 320 | 321 | ✓ gets no default view if no defaultView setting 322 | 323 | ✓ gets no default view if no default View deleted 324 | 325 | ✓ gets no default folder if no settings 326 | 327 | ✓ gets no default folder if no setting 328 | 329 | ✓ sets and gets default folders 330 | 331 | ✓ gets people and adds avatars 332 | 333 | ✓ gets people and generates avatar or uses given avatar 334 | 335 | ✓ get people handles no people object 336 | 337 | ✓ get people handles empty people object 338 | 339 | ✓ publishes changed events 340 | 341 | ✓ can add a selection set 342 | 343 | ✓ can add a folder with a selection set reference 344 | 345 | ✓ can add a task with a selection set field 346 | ref.obj undefined 347 | entry { id: '59d4eb70-481a-4528-b9fa-c0e375094aa5', 348 | type: 'folder', 349 | modified: { userId: 'mock-user', userTime: '2015-10-26T14:24:35.672Z' }, 350 | sync: { local: { state: 'added' } }, 351 | created: { userId: 'mock-user', userTime: '2015-10-26T14:24:35.672Z' }, 352 | name: 'Folder', 353 | data: {}, 354 | ref: 355 | { selectionSets: 356 | [ '4f768878-69a6-4e58-82e8-37420ceeb92d', 357 | '15fe85df-f9f9-4420-9d87-f5745841d1a4' ] } } 358 | 359 | ✓ can add and remove selection set refs 360 | 361 | catalog client query api permissions 362 | 363 | ✓ provides permitted actions of `read` for views 364 | 365 | ✓ provides permitted actions of `read` and `write` for formData 366 | 367 | ✓ provides NO permitted actions for item in folder f1 368 | 369 | catalog client query api 370 | 371 | ✓ find exposes isAttached 372 | 373 | ✓ find exposes complete 374 | 375 | ✓ find can match on assignedTo field 376 | 377 | ✓ find can match on assignedBy field 378 | 379 | ✓ find returns empty results if no assignedTo/By match is found 380 | 381 | ✓ find exposes isScene if view contains filters,layouts models or selects and respects isScene query param 382 | 383 | ✓ find exposes/projects labels 384 | 385 | ✓ find exposes/projects folders (38ms) 386 | 387 | ✓ find works for names 388 | at Context. (test/hash-fs.spec.js:26:8) 389 | 390 | ✓ uses cache for incremental name search and pagination 391 | 392 | ✓ find works for ids 393 | 394 | ✓ find works for BIM objects 395 | 396 | ✓ find works for templates 397 | 398 | ✓ find works for labels 399 | 400 | ✓ find works for modified time of now 401 | 402 | ✓ find works for modified time 403 | 404 | ✓ find works for folders 405 | 406 | ✓ find works for root folder 407 | 408 | ✓ find works for folderAliases 409 | 410 | ✓ find ANDs arguments 411 | 412 | ✓ find works for type 413 | 414 | ✓ find handles entries with no ref when searching by anything 415 | 416 | ✓ find handles entries with no ref.id when searching by id 417 | 418 | ✓ find handles entries with no ref.obj when searching by obj 419 | 420 | ✓ find works for forms - completed or otherwise, and can match due dates 421 | 422 | ✓ find ignores removed by default 423 | 424 | ✓ find does not ignore removed if required 425 | 426 | ✓ find reports folder 427 | 428 | ✓ omits empty folders (45ms) 429 | 430 | ✓ find exposes colour for label entries 431 | 432 | ✓ finds forms modified-by people 433 | 434 | ✓ finds forms assigned-to people 435 | 436 | ✓ finds forms modified OR assigned-to people 437 | 438 | ✓ finds overdue forms 439 | 440 | ✓ sorts on name 441 | 442 | ✓ sorts folders first if asked 443 | 444 | ✓ natural sorts on name 445 | 446 | ✓ sorts on modified time 447 | 448 | ✓ sorts on dueDate 449 | 450 | ✓ sorts plans by elevation and projects level name and elevation 451 | 452 | ✓ limit limits 453 | at Context. (test/hash-fs.spec.js:26:8) 454 | 455 | ✓ limit returns first results if no offset 456 | 457 | ✓ limit tells you if there are no more results 458 | 459 | ✓ limit handles size bigger than all results 460 | 461 | ✓ limit handles size bigger than remaining results 462 | 463 | ✓ limit handles offset beyond end of results 464 | 465 | ✓ limit handles no results 466 | 467 | ✓ limit handles bad limits 468 | 469 | ✓ limit includes only non-removed item in size by default 470 | 471 | ✓ provides modified by user name 472 | 473 | ✓ can find by ids 474 | 475 | ✓ can find selection sets 476 | 477 | ✓ can find entries that have a selection set 478 | 479 | ✓ can find entries that have a selection set 480 | 481 | catalog-cobie-api 482 | 483 | ✓ should return a no bim queries 484 | 485 | ✓ should return a no bim queries 486 | 487 | ✓ should return a no bim queries 488 | 489 | ✓ should return a bim-query for single element 490 | 491 | ✓ should return selection-set queries 492 | 493 | ✓ should not fail on dead refs 494 | 495 | catalog conflict service 496 | 497 | ✓ has conflicts when there are conflicts 498 | 499 | ✓ has no conflicts when none 500 | 501 | ✓ has no conflicts when empty 502 | 503 | ✓ gets all conflicts 504 | 505 | ✓ gets all conflicts when none 506 | 507 | ✓ gets all conflicts when empty catalog 508 | 509 | ✓ gets all conflicts when no remote entry 510 | 511 | ✓ resolves using mine 512 | 513 | ✓ resolves using theirs (inline) 514 | 515 | ✓ resolves using theirs (resource download) 516 | 517 | ✓ resolve gives error if entry removed 518 | 519 | ✓ resolve gives error if entry no longer conflicting 520 | 521 | catalog database diy 522 | 523 | ✓ getEntryById returns an entry 524 | 525 | ✓ getEntryById returns undefined if none 526 | 527 | ✓ queries 528 | 529 | ✓ query returns nothing if no match 530 | 531 | ✓ inserts one entry 532 | 533 | ✓ inserts many entries 534 | 535 | ✓ insert many does nothing with empty array 536 | 537 | ✓ updates one entry 538 | 539 | ✓ updates many entries 540 | 541 | ✓ removes one entry 542 | 543 | ✓ removes many entries 544 | 545 | ✓ remove many does nothing with empty array 546 | 547 | ✓ loads catalogs once 548 | 549 | ✓ matches any 550 | 551 | catalog-merger 552 | 553 | ✓ merges all states of file resources one by one 554 | 555 | ✓ merges all states of file resources in one go (69ms) 556 | 557 | ✓ remembers a new download job during delta merges 558 | 559 | ✓ remembers an update download job during delta merges 560 | 561 | ✓ merges all basic states of inline data entries one by one (49ms) 562 | 563 | ✓ merges all basic states of inline data in one go (103ms) 564 | 565 | ✓ uploads just entry if resource has not changed (optimisation) 566 | 567 | ✓ downloads (merges) just entry if resource has not change (optimisation) 568 | 569 | ✓ preserves modify state 570 | 571 | ✓ merges remote addition twice giving the same result 572 | 573 | ✓ add then modify before sync uploads 574 | 575 | ✓ add then remove locally causes remove local only (no remove_remote) 576 | 577 | ✓ does not touch transfer objects 578 | 579 | ✓ handles empty local 580 | 581 | ✓ handles empty remote 582 | 583 | ✓ handles empty local and remote 584 | at Context. (test/hash-fs.spec.js:26:8) 585 | 586 | ✓ handles some erroneous edge cases 587 | 588 | catalog sync api 589 | 590 | ✓ flags jobs as complete 591 | 592 | ✓ flags download as complete 593 | 594 | ✓ updates ssn on first transfer 595 | 596 | ✓ gets all jobs 597 | 598 | ✓ removes entries without resources 599 | 600 | ✓ removes entries with resources 601 | 602 | ✓ removes entries with resources that have not been downloaded 603 | 604 | ✓ marks entries as unsynchronised 605 | 606 | ✓ merges a delta catalog 607 | 608 | ✓ merges a delta catalog using given catalog API 609 | 610 | catalog-write-service 611 | 612 | ✓ should reject adding entry with missing path 613 | 614 | ✓ should reject adding entry with missing name 615 | 616 | ✓ should reject adding entry with missing type 617 | 618 | ✓ should reject adding entry with missing data 619 | 620 | events 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | catalog import matcher 631 | 632 | ✓ finds nearest one, no exact matches 633 | 634 | ✓ finds nearest one, some exact matches 635 | 636 | ✓ handles empty query results 637 | 638 | ✓ handles elevation zero 639 | 640 | cobie-createForEntries 641 | { COBie: { creating: 'COBie data', from: { entries: 0 } } } 642 | create-cobie-data: 7ms 643 | 644 | ✓ should create nothing 645 | { COBie: { creating: 'COBie data', from: { entries: 1 } } } 646 | create-cobie-data: 4ms 647 | 648 | ✓ should link single unrelated document to Facility 649 | { COBie: { creating: 'COBie data', from: { entries: 1 } } } 650 | create-cobie-data: 3ms 651 | 652 | ✓ should link single document to correct componenet 653 | at Context. (test/hash-fs.spec.js:26:8) 654 | 655 | entrySet 656 | 657 | ✓ should create an object from array 658 | 659 | ✓ should be pure func 660 | at Context. (test/hash-fs.spec.js:26:8) 661 | 662 | ✓ should only have a single thing 663 | 664 | ✓ should have a two things 665 | 666 | cobie-base64 667 | 668 | ✓ should encode same as C# 669 | 670 | ✓ should return an IFC guid for a revitId 671 | 672 | ✓ should be able to covert from revItId -> b64IfcGuid 673 | 674 | cobie matchMaker 675 | 676 | ✓ should return defualt ref if no match for ref.element 677 | 678 | ✓ should return relationship if its a match 679 | 680 | ✓ should return two relationships 681 | 682 | ✓ should default relationship when the ifc-el-id is not in knownIfcIds 683 | 684 | ✓ should create relation ship to selectionSet elements 685 | 686 | ✓ should produce a COBie document 687 | 688 | cobie pathDeDupe 689 | 690 | ✓ should not change any entry 691 | 692 | ✓ should modify guid-3 entry 693 | 694 | ✓ should modify, guid-2 entry name and guid-3 entry name 695 | 696 | ✓ should not modify just because of (x) 697 | 698 | cobie buildWorkbook 699 | when building workbookdata from COBie data as JSON 700 | 701 | ✓ should add headings when createCobieDocumentsSheet 702 | 703 | ✓ should color data correctly when createCobieDocumentSheet 704 | 705 | ✓ should populate data correctly when createCobieDocumentSheet 706 | 707 | ✓ should add headings when createCobieContactSheet 708 | 709 | ✓ should populate data correctly when createCobieContactSheet 710 | 711 | ✓ should color cells correctly when createCobieContactSheet 712 | 713 | ✓ should include extra field at the end 714 | 715 | ✓ should include the instruction sheet 716 | with some COBie data as JSON 717 | 718 | ✓ should generate a mapping of ifc-guids to sheet refs 719 | 720 | Default folder service 721 | 722 | ✓ gets hardcoded defaults if none set 723 | 724 | ✓ gets hardcoded defaults if catalog error 725 | 726 | ✓ gets hardcoded defaults if no setting 727 | 728 | ✓ gets merged defaults 729 | 730 | ✓ get projects folder 731 | 732 | ✓ set saves new values, removed not set values and returns new default folders 733 | 734 | ✓ set removes old values 735 | 736 | ✓ set does nothing if no change 737 | 738 | ✓ set handles empty input 739 | 740 | ✓ set handles no setting 741 | 742 | document service 743 | 744 | ✓ Opens and starts tracking a doc if not already tracked 745 | 746 | ✓ Tracks documents with the same name 747 | 748 | ✓ Opens existing if already tracking 749 | 750 | ✓ Open fails if file in use 751 | 752 | ✓ Publish modifies doc and carries on tracking 753 | 754 | ✓ Revert stops tracking and unflags for edit 755 | 756 | ✓ Revert does not stop tracking on error 757 | 758 | entry-filters 759 | 760 | ✓ should leave entries unchanged 761 | 762 | ✓ should remove entries that are not readable by user 763 | 764 | entry projector 765 | 766 | ✓ produces `ok` summary state for complete entries 767 | 768 | ✓ produces `unsynchronised` summary state for unsynchronised entries 769 | 770 | ✓ produces `ok` summary state for newly added local entries 771 | 772 | ✓ produces `unpublished` summary state for newly added local unpublished entries 773 | 774 | ✓ produces `no_resource` summary state for resource entries that have no file 775 | 776 | ✓ produces `removed` summary state for resource entries that have been removed 777 | 778 | ✓ produces `out_of_date` summary state for entries with empty sync objects 779 | 780 | entry urls 781 | 782 | ✓ should return a remote URL when not cached 783 | 784 | ✓ should return a local url when cached 785 | 786 | ✓ should err non document entry 787 | 788 | ✓ should err non hash resource 789 | 790 | ✓ should have a download link 791 | 792 | fifo 793 | 794 | ✓ should create the file needed for persistance 795 | with nothing persisted 796 | 797 | ✓ should persist data to disk 798 | 799 | ✓ should keep track of _top of fifo 800 | 801 | ✓ should calback with bytes pushed 802 | 803 | ✓ should not have readable data before its persisted 804 | 805 | ✓ should not error on peek 806 | 807 | ✓ should callback with all readable data 808 | 809 | ✓ should not read poped data 810 | 811 | ✓ should persist pointers to disk on pop 812 | with persisted data 813 | 814 | ✓ should read persisted pointers 815 | 816 | ✓ should read all data up to request size 817 | with > 5Mb of persisted data 818 | 819 | ✓ should left-tuncate file to keep newest 5Mb 820 | 821 | ✓ should write pointer area during truncate 822 | after trucated 823 | 824 | ✓ should have correct pointers 825 | 826 | filing spec builder 827 | 828 | ✓ returns error when no base folder has been specified 829 | 830 | ✓ returns error when first dynamic path does not use variable 831 | 832 | ✓ returns error when dynamic path contains unsupported variable 833 | 834 | ✓ creates valid spec if no dynamic paths are used 835 | 836 | ✓ creates valid spec if dynamic path is changed 837 | 838 | ✓ creates valid spec from existing spec when valid dynamic paths are added 839 | 840 | ✓ creates valid spec from existing spec when dynamic paths and base folder are modified 841 | 842 | filing spec factory 843 | 844 | ✓ creates a valid builder with root base folder if project settings does not exist 845 | 846 | ✓ creates a valid builder with root base folder if project settings does not contain a default task folder 847 | 848 | ✓ creates a valid builder with task folder if project settings contains a default task folder 849 | 850 | hash filesystem 851 | 852 | ✓ can store a json object 853 | 854 | ✓ can read back json object 855 | 856 | 1) "after all" hook 857 | 858 | hash filesystem 859 | 860 | ✓ should add an array of objects 861 | 862 | ✓ should add an array of paths 863 | 864 | catalog import-service 865 | 866 | ✓ handles nothing to import 867 | 868 | ✓ imports if no existing catalog, modelId added to plans 869 | 870 | ✓ matches existing views 871 | 872 | ✓ matches existing plans, says which to remove, ignores plans for other models 873 | 874 | ✓ handles undefined plan entry 875 | 876 | ✓ matches existing 3D clip plans 877 | 878 | ✓ matches existing sheets 879 | 880 | ✓ prepares plans, views and sheets 881 | 882 | ✓ prepares if no plan view or sheet 883 | 884 | ✓ should make names safe according to validation rule 885 | 886 | ✓ adds model IDs 887 | 888 | ✓ addModelId handles no plans 889 | when model description file has 1 view, 1 plan and 1 sheet 890 | 891 | ✓ should return a single viewDefinition 892 | 893 | ✓ should return a single planDefinition 894 | 895 | ✓ should return a single sheetDefinition 896 | 897 | ✓ should sanitise names 898 | 899 | natural sort sorts 900 | 901 | ✓ does not just alphanum sort 902 | 903 | ✓ sorts undefined/null last 904 | 905 | ✓ Does not blow up with non string input 906 | 907 | people service 908 | 909 | ✓ gets current user 910 | 911 | ✓ gets a user by id 912 | 913 | ✓ gives error if user not found 914 | 915 | permissions service 916 | module API 917 | 918 | ✓ should get permissions from catalog 919 | 920 | ✓ should get user id from session 921 | 922 | ✓ should work for a single entry id 923 | 924 | ✓ should work for a entry object 925 | 926 | ✓ should work for a user in a group 927 | 928 | ✓ should return common actions 929 | 930 | ✓ honours folder acl create permissions 931 | 932 | ✓ allows project settings to be changed 933 | 934 | ✓ does not allows project settings to be changed 935 | 936 | persistent-object 937 | 938 | ✓ should provide get and save methods from its public interface 939 | 940 | ✓ reads from disk on first get invocation 941 | 942 | ✓ reads from cached object on n+1 invocation 943 | 944 | ✓ persists current object to file on save 945 | 946 | ✓ persists new object to file on save 947 | 948 | ✓ save handles no callback 949 | 950 | Server XHR Factory 951 | 952 | ✓ Returns null error, parsed response, server status code and XHR client on success 953 | 954 | ✓ Defaults to GET 955 | 956 | ✓ Allows override of method 957 | 958 | ✓ Sends no session token if none provided 959 | 960 | ✓ Adds session token if asked 961 | 962 | ✓ Gives a NetworkError if no response from server 963 | 964 | ✓ Gives generic error on server error but still gives response and status code 965 | 966 | ✓ Parses JSON error responses when arraybuffer requested 967 | 968 | ✓ Gives empty JSON response when arraybuffer requested, but server returns invalid JSON error 969 | 970 | download file in chunks 971 | 972 | ✓ can download a file 973 | 974 | ✓ can download a 0 byte file 975 | 976 | ✓ stops on network or file error 977 | 978 | ✓ stops on http error code 979 | 980 | ✓ stops on bad transfer size 981 | 982 | ✓ stops on negative transfer size 983 | 984 | ✓ stops on zero transfer size 985 | 986 | ✓ stops when file too big 987 | 988 | sync project priorities 989 | 990 | ✓ fetches all, merges N catalogs, gets jobs 991 | 992 | ✓ handles fetch fail 993 | 994 | ✓ handles no projects 995 | 996 | ✓ handles merge fail 997 | 998 | sync-job-heuristics 999 | 1000 | ✓ should download job with heuristicsOverriden 1001 | 1002 | ✓ should skip jobs when sync.job.action equals download 1003 | 1004 | sync job processor factory 1005 | 1006 | ✓ Null processor for resolve_conflict 1007 | 1008 | ✓ should create an upload job 1009 | 1010 | ✓ should create an download job 1011 | 1012 | ✓ should create a remove_local job 1013 | 1014 | ✓ should create a remove_remote job 1015 | 1016 | ✓ Null processor and warning for unknown job 1017 | 1018 | sync job queue 1019 | 1020 | ✓ prioritises folder removals after all other removals within a project 1021 | 1022 | ✓ prioritises folder removals last per project 1023 | 1024 | ✓ does not return jobs in projects with sync off 1025 | 1026 | sync job scheduler 1027 | 1028 | ✓ Consumes all jobs and does the right thing for each 1029 | 1030 | ✓ handles no jobs (no log, no events, no processor created) 1031 | 1032 | ✓ skips jobs it should skip (no events, log) 1033 | 1034 | ✓ Silently ignores jobs with no processor 1035 | 1036 | ✓ Logs job failure and notifies history of failure 1037 | 1038 | ✓ logs network failure, but does not record as jobHistory Failure 1039 | 1040 | ✓ carries on processing after failure 1041 | 1042 | ✓ generates conflict events 1043 | 1044 | sync project priorities 1045 | 1046 | ✓ gives defaults if none 1047 | 1048 | ✓ remembers settings 1049 | 1050 | ✓ builds list of synchronising projects 1051 | 1052 | ✓ should always have priorities for each project 1053 | 1054 | task api 1055 | 1056 | ✓ can add a task with empty submission 1057 | 1058 | ✓ can modify a task with empty submission and retain it's folder 1059 | 1060 | ✓ can add a task with two attachments 1061 | 1062 | ✓ can modify a task with two attachments and retain it's folder 1063 | 1064 | ✓ can add a filing spec to a task 1065 | 1066 | ✓ rejects adding filing spec to anything but a template 1067 | 1068 | ✓ copies first instance of assign data from submission to entry 1069 | 1070 | ✓ does not copy assign data if any of the fields are not strings (userids) 1071 | 1072 | ✓ can add a task with a selection field 1073 | 1074 | ✓ can modify a task with a selection field and retain it's folder 1075 | 1076 | task filing service 1077 | 1078 | ✓ expands variables correctly 1079 | 1080 | ✓ builds correct path representations when base folder exists 1081 | 1082 | ✓ handles exception of root base folder when building path representations 1083 | 1084 | ✓ returns error when building path representations if base folder does not exist 1085 | 1086 | ✓ returns correct list of supported variables 1087 | 1088 | ✓ can evaluate all variables 1089 | 1090 | ✓ creates the correct folders when executing a filing spec 1091 | 1092 | ✓ creates the correct folders when base folder is root 1093 | 1094 | ✓ skips creating a folder if it exists when executing a filing spec 1095 | 1096 | ✓ saves to default tasks folder if it exists and base folder cannot be found 1097 | 1098 | view history service 1099 | 1100 | ✓ does nothing if no history 1101 | 1102 | ✓ goes back and forward 1103 | 1104 | ✓ will not add the same view twice 1105 | 1106 | ✓ will add the same camera pos for different modes 1107 | 1108 | ✓ lists no recent views if none 1109 | 1110 | ✓ lists only named views as recent 1111 | 1112 | ✓ de-dupes recent views 1113 | 1114 | 1115 | 405 passing (4s) 1116 | 1 failing 1117 | 1118 | 1) hash filesystem "after all" hook: 1119 | TypeError: fs.rmdir is not a function 1120 | at Context. (test/hash-fs.spec.js:26:8) 1121 | 1122 | 1123 | -------------------------------------------------------------------------------- /test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "parse.h" 5 | #include "editor.h" 6 | #include "input.h" 7 | 8 | #define assert_zu(a, b) if(a != b){ fprintf(stderr, "FAILURE (line %d): '%zu' != '%zu' (" #a " != " #b ")\n", __LINE__, (size_t)a, (size_t)b); exit(1); } 9 | #define assert_str(a, b) if(0 != strcmp(a, b)){ fprintf(stderr, "FAILURE (line %d): '%s' != '%s'\n", __LINE__, a, b); exit(1); } 10 | 11 | #define assert_field(field, value) assert(0 == strncmp(str+field.start, value, field.stop-field.start)); 12 | 13 | #define assert_cmd(format, result, field) \ 14 | strcpy(cmd, format); \ 15 | format_cmd(cmd, str, &field, NULL); \ 16 | assert_str(cmd, result); 17 | 18 | #define assert_skipped_sequence(seq) \ 19 | assert_line_length(seq "foo\n", strlen(seq)+4, 4); 20 | 21 | void 22 | readfile (char* s, const char* filename) 23 | { 24 | FILE* fd = fopen(filename, "r"); 25 | assert(fd); 26 | fread(s, sizeof(char), BUFSIZ, fd); 27 | fclose(fd); 28 | } 29 | 30 | void 31 | test_replace () 32 | { 33 | const char* s = "--foobar.js--13--14--"; 34 | 35 | char buf[BUFSIZ]; 36 | memset(buf, 0, BUFSIZ); 37 | strcpy(buf, "vim +%d"); 38 | replace(buf, "%d", s+13, 2); 39 | assert_str(buf, "vim +13"); 40 | 41 | strcpy(buf, "vim +%d:%d"); 42 | replace(buf, "%d", s+13, 2); 43 | replace(buf, "%d", s+17, 2); 44 | assert_str(buf, "vim +13:14"); 45 | 46 | strcpy(buf, "subl %s:%d"); 47 | replace(buf, "%s", s+2, 9); 48 | replace(buf, "%d", s+13, 2); 49 | assert_str(buf, "subl foobar.js:13"); 50 | } 51 | 52 | void 53 | input_file (input_t* in, const char* filename, int width) 54 | { 55 | FILE* fd = fopen(filename, "r"); 56 | if(!fd) 57 | { 58 | fprintf(stderr, "ERROR: Missing fixture file %s\n", filename); 59 | exit(1); 60 | } 61 | input_init(in); 62 | while (input_read(in, fileno(fd), width, 0)) 63 | ; 64 | fclose(fd); 65 | } 66 | 67 | void 68 | assert_line_length (char* s, int n, int width) 69 | { 70 | const char* next_line = find_next_line(s, width); 71 | assert(next_line); 72 | assert_zu((next_line - s), n); 73 | } 74 | 75 | void 76 | assert_consumed (char* s) 77 | { 78 | assert(consume_escape_seq(s) == s+strlen(s)); 79 | } 80 | 81 | void 82 | test_lines () 83 | { 84 | input_t in; 85 | 86 | assert_consumed("\033[1m"); 87 | assert_consumed("\033[1;2m"); 88 | 89 | assert_line_length("foo\nbar", 4, 80); 90 | assert_line_length("\nbar", 1, 80); 91 | assert_line_length("foobarbaz", 4, 4); 92 | 93 | assert_skipped_sequence("\033[1m"); 94 | 95 | input_file(&in, "samples/errors.log", 80); 96 | assert_zu(in.nlines, 10); 97 | assert(in.line_offsets[3] == 22); 98 | input_free(&in); 99 | 100 | input_file(&in, "samples/errors.log", 10); 101 | assert_zu(in.nlines, 21); 102 | assert(in.line_offsets[3] == 21); 103 | input_free(&in); 104 | 105 | input_file(&in, "samples/testing-big.txt", 80); 106 | assert(find_line_index(&in, 1) == 1); 107 | assert(find_line_index(&in, 3) == 1); 108 | assert(find_line_index(&in, 64) == 6); 109 | assert(find_line_index(&in, 531) == 17); 110 | input_free(&in); 111 | } 112 | 113 | void 114 | test_parse () 115 | { 116 | char cmd[BUFSIZ]; 117 | char str[BUFSIZ]; 118 | 119 | pattern_list_t patterns; 120 | init_patterns(&patterns); 121 | add_default_patterns(&patterns); 122 | 123 | readfile(str, "samples/simple.txt"); 124 | 125 | assert(study(&patterns, str, strlen(str), 0)); 126 | 127 | assert(field_count == 5); 128 | 129 | assert_field(field_offsets[0].path, "foobar.js"); 130 | assert_field(field_offsets[0].line, "16"); 131 | assert(field_offsets[0].column.start == 0); 132 | 133 | assert_cmd("vim +'call cursor(%d, %d)'", "vim +'call cursor(16, 0)' foobar.js", field_offsets[0]); 134 | 135 | assert_field(field_offsets[2].path, "foobar.js"); 136 | assert_field(field_offsets[2].line, "14"); 137 | assert_field(field_offsets[2].column, "6"); 138 | 139 | assert_cmd("subl %s:%d", "subl foobar.js:14", field_offsets[2]); 140 | assert_cmd("vim +%d", "vim +14 foobar.js", field_offsets[2]); 141 | assert_cmd("vim +'call cursor(%d, %d)'", "vim +'call cursor(14, 6)' foobar.js", field_offsets[2]); 142 | } 143 | 144 | void 145 | test_editor () 146 | { 147 | char* cmd; 148 | 149 | assert(is_named_executable("foo", "foo")); 150 | assert(is_named_executable("foo -test", "foo")); 151 | assert(is_named_executable("/usr/bin/foo", "foo")); 152 | assert(is_named_executable("/usr/bin/foo -test", "foo")); 153 | 154 | unsetenv("EDITOR"); 155 | unsetenv("PLS"); 156 | 157 | cmd = editor_command(); 158 | assert_str(cmd, EDITOR_COMMAND); 159 | 160 | setenv("EDITOR", "atom", 1); 161 | cmd = editor_command(); 162 | assert_str(cmd, "atom %s:%d:%d"); 163 | 164 | setenv("EDITOR", "/usr/bin/subl -w", 1); 165 | cmd = editor_command(); 166 | assert_str(cmd, "/usr/bin/subl -w %s:%d:%d"); 167 | 168 | setenv("PLS", "Foo bar", 1); 169 | cmd = editor_command(); 170 | assert_str(cmd, "Foo bar"); 171 | } 172 | 173 | int main(int argc, char const *argv[]) 174 | { 175 | (void)argc; (void)argv; 176 | 177 | test_editor(); 178 | 179 | test_lines(); 180 | 181 | test_replace(); 182 | 183 | test_parse(); 184 | 185 | return 0; 186 | } 187 | --------------------------------------------------------------------------------